Skip to content
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
7e7d331
docs: add adapter HA support design spec
qevolg May 20, 2026
be11224
feat: add failover implementation guide for Node.js applications usin…
qevolg May 26, 2026
5da186d
feat: update uuid dependency to version 11.1.1
qevolg May 26, 2026
8f24c6e
feat: enable contrib build for TDengine and update taosadapter instal…
qevolg May 26, 2026
9c1557f
feat: update tsconfig paths to use relative paths for module resolution
qevolg May 27, 2026
2e9aeda
feat: enhance adapter HA support with dynamic instance discovery and …
qevolg May 27, 2026
0383996
feat: remove outdated adapter HA design specification document
qevolg May 27, 2026
ac44f11
docs: add adapter HA design spec for Node.js connector
qevolg May 27, 2026
d15273e
docs: fix adapter HA design spec per review feedback
qevolg May 27, 2026
906bb59
docs: address round-2 review on adapter HA design
qevolg May 27, 2026
0f37a64
feat: implement adapter HA support with cluster registry and endpoint…
qevolg May 27, 2026
d528471
feat: enhance ClusterRegistry with reset functionality and add tests …
qevolg May 27, 2026
6674d45
feat: remove outdated failover practice documentation for Node.js on …
qevolg May 27, 2026
713b4c5
feat: remove adapter HA design documentation for Node.js connector
qevolg May 27, 2026
9d5b2a8
docs: add cluster type design spec
qevolg May 28, 2026
0897e1b
docs: handle cross-cluster seeds in getOrCreateCluster
qevolg May 28, 2026
5e35ac6
docs: add _failoverAddresses removal to cluster design
qevolg May 28, 2026
ef24bf4
docs: refine registerCluster and mergeDiscoveredEndpoints design
qevolg May 28, 2026
4b4d9a6
docs: rename registerCluster to updateCluster, return void
qevolg May 28, 2026
f864dfb
docs: apply review feedback to cluster design spec
qevolg May 28, 2026
11d1621
docs: update getOrCreateCluster behavior to avoid binding unmapped seeds
qevolg May 28, 2026
af87d0b
feat: implement Cluster class and enhance ClusterRegistry for improve…
qevolg May 28, 2026
001bd4c
feat: remove Cluster type design document and related specifications
qevolg May 28, 2026
2c25d57
refactor: improve address handling in parseDiscoveredEndpoints and up…
qevolg May 28, 2026
4fec967
fix: remove unnecessary address mapping in createBareConnector and up…
qevolg May 28, 2026
7a4b4b6
refactor: remove _resetForTest method and update test cases to reset …
qevolg May 28, 2026
515a746
feat: add tests for adapter_ha functionality in WsSql and WsConsumer,…
qevolg May 28, 2026
7049e06
feat: implement adapter_ha pooled connector tests and refactor relate…
qevolg May 28, 2026
0ae6ccb
feat: add tests for adapter_ha pooled connector and refactor related …
qevolg May 28, 2026
5bbd928
feat: update version to 3.5.0 in package.json and package-lock.json
qevolg May 29, 2026
aeca0ae
feat: update uuid package version to 9.0.1 and refactor Cluster class…
qevolg May 29, 2026
b4b342c
feat: add Grype configuration for vulnerability scanning of uuid package
qevolg May 29, 2026
3d9eaed
feat: remove deprecated uuid package metadata from package-lock.json
qevolg May 29, 2026
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
24 changes: 22 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,31 @@ jobs:
cd TDengine
mkdir debug
cd debug
cmake .. -DBUILD_HTTP=false -DBUILD_JDBC=false -DBUILD_TOOLS=false -DBUILD_TEST=off -DBUILD_DEPENDENCY_TESTS=false
cmake .. -DBUILD_JDBC=false -DBUILD_TOOLS=false -DBUILD_TEST=off -DBUILD_DEPENDENCY_TESTS=false -DBUILD_CONTRIB=ON
make -j 4
sudo make install
which taosd
which taosadapter

- name: Checkout taosadapter
uses: actions/checkout@v4
with:
repository: "taosdata/taosadapter"
path: "taosadapter"
ref: "main"
Comment thread
qevolg marked this conversation as resolved.
Comment thread
qevolg marked this conversation as resolved.

- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: "1.25.10"
cache-dependency-path: taosadapter/go.sum

- name: Build and install taosadapter
run: |
cd taosadapter
go mod download
go build -o taosadapter
sudo install -m 755 taosadapter /bin/taosadapter
taosadapter --version

- name: Start taosd
run: nohup sudo taosd &
Expand Down
8 changes: 5 additions & 3 deletions nodejs/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion nodejs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
"json-bigint": "^1.0.0",
"moment-timezone": "^0.5.45",
"typescript": "^5.3.3",
"uuid": "^9.0.1",
"uuid": "^11.1.1",
Comment thread
qevolg marked this conversation as resolved.
Outdated
Comment thread
qevolg marked this conversation as resolved.
Outdated
"websocket": "^1.0.34",
Comment thread
qevolg marked this conversation as resolved.
"winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0"
Expand Down
138 changes: 138 additions & 0 deletions nodejs/src/client/clusterRegistry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { randomUUID } from "crypto";
Comment thread
qevolg marked this conversation as resolved.
Outdated
Comment thread
qevolg marked this conversation as resolved.
Outdated
import { Address, mergeAddresses } from "../common/dsn";
Comment thread
qevolg marked this conversation as resolved.
import logger from "../common/log";

export class Cluster {
readonly id: string;
private _addresses: readonly Address[];

constructor(addresses: Address[]) {
this.id = randomUUID();
this._addresses = Cluster.freezeAddresses(addresses);
}

get addresses(): readonly Address[] {
return this._addresses;
}

addAddresses(addresses: Address[]): void {
const merged = mergeAddresses([...this._addresses], addresses);
this._addresses = Cluster.freezeAddresses(merged);
}

private static freezeAddresses(addresses: Address[]): readonly Address[] {
return Object.freeze(
addresses.map((address) =>
Object.freeze(new Address(address.host, address.port))
)
);
}
}

export class ClusterRegistry {
private static _instance?: ClusterRegistry;
private endpointToCluster: Map<string, Cluster> = new Map();

private constructor() { }

public static instance(): ClusterRegistry {
if (!ClusterRegistry._instance) {
ClusterRegistry._instance = new ClusterRegistry();
}
return ClusterRegistry._instance;
}

private endpointKey(address: Address): string {
return `${address.host}:${address.port}`;
}

private collectMatchedClusters(addresses: Address[]): Map<string, Cluster> {
const matchedClusters = new Map<string, Cluster>();
for (const address of addresses) {
const cluster = this.endpointToCluster.get(this.endpointKey(address));
if (!cluster) {
continue;
}
matchedClusters.set(cluster.id, cluster);
}
return matchedClusters;
}

public getOrCreateCluster(seeds: Address[]): Cluster | null {
const matchedClusters = this.collectMatchedClusters(seeds);
if (matchedClusters.size > 1) {
logger.warn(
"Adapter HA: seed addresses span multiple known clusters, " +
"skipping expansion. Ensure all seeds belong to the same cluster."
);
return null;
}
if (matchedClusters.size === 1) {
return matchedClusters.values().next().value as Cluster;
}

const cluster = new Cluster(seeds);
for (const seed of seeds) {
this.endpointToCluster.set(this.endpointKey(seed), cluster);
}
return cluster;
}

public updateCluster(discovered: Address[]): void {
if (discovered.length === 0) {
return;
}

const matchedClusters = this.collectMatchedClusters(discovered);
if (matchedClusters.size === 0) {
return;
}

if (matchedClusters.size > 1) {
logger.warn(
"Adapter HA: discovered endpoints match multiple known clusters, skipping update."
);
return;
}

const cluster = matchedClusters.values().next().value as Cluster;
cluster.addAddresses(discovered);
for (const address of discovered) {
this.endpointToCluster.set(this.endpointKey(address), cluster);
}
}

public expandEndpoints(seeds: Address[]): Address[] {
let matchedCluster: Cluster | null = null;

for (const seed of seeds) {
const cluster = this.endpointToCluster.get(this.endpointKey(seed));
if (!cluster) {
continue;
}

if (matchedCluster === null) {
matchedCluster = cluster;
continue;
}

if (matchedCluster.id !== cluster.id) {
logger.warn(
"Adapter HA: seed addresses span multiple known clusters, " +
"skipping expansion. Ensure all seeds belong to the same cluster."
);
return seeds.map((seedAddress) =>
new Address(seedAddress.host, seedAddress.port)
);
}
}

if (!matchedCluster) {
return seeds.map((seedAddress) =>
new Address(seedAddress.host, seedAddress.port)
);
}

return mergeAddresses(seeds, [...matchedCluster.addresses]);
}
}
20 changes: 18 additions & 2 deletions nodejs/src/client/wsClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@ export class WsClient {
}
}

private buildConnMessage(database?: string | undefined | null) {
private buildConnMessage(
database?: string | undefined | null,
listInstances?: boolean
) {
return {
action: "conn",
args: {
Expand All @@ -61,6 +64,7 @@ export class WsClient {
...(this._userApp && { app: this._userApp }),
...(this._userIp && { ip: this._userIp }),
...(this._bearerToken && { bearer_token: this._bearerToken }),
...(listInstances !== undefined && { list_instances: listInstances }),
},
};
}
Expand Down Expand Up @@ -140,7 +144,8 @@ export class WsClient {
}

async connect(database?: string | undefined | null): Promise<void> {
const connMsg = this.buildConnMessage(database);
const listInstances = this._dsn.isAdapterHA() ? true : undefined;
const connMsg = this.buildConnMessage(database, listInstances);
const normalizedDatabase = this.normalizeConnectedDatabase(database ?? null);
if (logger.isDebugEnabled()) {
logger.debug("[wsClient.connect.connMsg]===>" + JSONBig.stringify(connMsg, (key, value) =>
Expand All @@ -165,6 +170,9 @@ export class WsClient {
if (result.msg.code == 0) {
this._connectedDatabase = normalizedDatabase;
this._wsConnector.markSessionReady();
if (result.msg.list_instances) {
this._wsConnector.mergeDiscoveredEndpoints(result.msg.list_instances);
}
return;
}
await this.close();
Expand Down Expand Up @@ -320,6 +328,14 @@ export class WsClient {
return this.getWsConnector().getReconnectRetries();
}

isAdapterHA(): boolean {
return this._dsn.isAdapterHA();
}

mergeDiscoveredEndpoints(instances: string[]): void {
this.getWsConnector().mergeDiscoveredEndpoints(instances);
}

async sendMsg(message: string): Promise<any> {
logger.debug("[wsClient.sendMsg]===>" + message);
return this.getWsConnector().sendMsg(message);
Expand Down
36 changes: 35 additions & 1 deletion nodejs/src/client/wsConnector.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { ICloseEvent, w3cwebsocket } from "websocket";
import { Address, Dsn } from "../common/dsn";
import {
Address,
Dsn,
mergeAddresses,
parseDiscoveredEndpoints
} from "../common/dsn";
import { AddressConnectionTracker } from "../common/addressConnectionTracker";
import {
ErrorCode,
Expand All @@ -12,6 +17,7 @@ import {
maskSensitiveForLog,
maskUrlForLog
} from "../common/utils";
import { ClusterRegistry } from "./clusterRegistry";

interface InflightRequest {
reqId: bigint;
Expand Down Expand Up @@ -179,6 +185,16 @@ export class WebSocketConnector {
}
this._poolKey = poolKey;
this._dsn = dsn;
if (dsn.isAdapterHA()) {
const expanded = ClusterRegistry.instance().expandEndpoints(this._dsn.addresses);
if (expanded.length > this._dsn.addresses.length) {
logger.info(
"Adapter HA: expanded seed endpoints from registry, " +
`${this._dsn.addresses.length} -> ${expanded.length}`
);
this._dsn.addresses = expanded;
}
}
this._currentAddress = this.selectLeastConnectedAddress();
this._retryConfig = RetryConfig.fromDsn(dsn);
this._inflightStore = new InflightRequestStore();
Expand Down Expand Up @@ -592,6 +608,24 @@ export class WebSocketConnector {
this._sessionReady = true;
}

public mergeDiscoveredEndpoints(instances: string[]): void {
if (!this._dsn.isAdapterHA() || !instances || instances.length === 0) {
return;
}
const discovered = parseDiscoveredEndpoints(instances);
ClusterRegistry.instance().updateCluster(discovered);
const merged = mergeAddresses(this._dsn.addresses, discovered);
Comment thread
qevolg marked this conversation as resolved.
Comment thread
qevolg marked this conversation as resolved.
Comment thread
qevolg marked this conversation as resolved.
Comment thread
qevolg marked this conversation as resolved.
Comment thread
qevolg marked this conversation as resolved.
Comment thread
qevolg marked this conversation as resolved.
if (merged.length <= this._dsn.addresses.length) {
return;
}

const newCount = merged.length - this._dsn.addresses.length;
this._dsn.addresses = merged;
logger.info(
`Adapter HA: discovered ${newCount} new endpoint(s), total ${merged.length}`
);
}

private async recoverSessionContext(): Promise<void> {
if (!this._sessionRecoveryHook) {
return;
Expand Down
11 changes: 9 additions & 2 deletions nodejs/src/client/wsConnectorPool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Dsn } from "../common/dsn";
import { ErrorCode, TDWebSocketClientError } from "../common/wsError";
import logger from "../common/log";
import { w3cwebsocket } from "websocket";
import { ClusterRegistry } from "./clusterRegistry";
import { WebSocketConnector } from "./wsConnector";

const mutex = new Mutex();
Expand Down Expand Up @@ -46,12 +47,18 @@ export class WebSocketConnectionPool {
}

private getPoolKey(dsn: Dsn): string {
const auth = this.buildAuthScope(dsn);
const path = dsn.path();
if (dsn.isAdapterHA()) {
const cluster = ClusterRegistry.instance().getOrCreateCluster(dsn.addresses);
if (cluster) {
return `${dsn.scheme}://${cluster.id}/${path}#auth=${auth}`;
}
}
const addrs = [...dsn.addresses]
.sort((a, b) => `${a.host}:${a.port}`.localeCompare(`${b.host}:${b.port}`))
.map((addr) => `${addr.host}:${addr.port}`)
.join(",");
const auth = this.buildAuthScope(dsn);
const path = dsn.path();
return `${dsn.scheme}://${addrs}/${path}#auth=${auth}`;
}

Expand Down
Loading
Loading