This is an automated email from the ASF dual-hosted git repository.

RongtongJin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/rocketmq-clients.git


The following commit(s) were added to refs/heads/master by this push:
     new 51561a16 enhancement(nodejs): isolate gRPC connections between client 
instances (#1383)
51561a16 is described below

commit 51561a16646bc82522406b13cb4e164df5ffa46f
Author: Quan <[email protected]>
AuthorDate: Tue Sep 22 17:34:02 2026 +0800

    enhancement(nodejs): isolate gRPC connections between client instances 
(#1383)
    
    - Pass 'grpc.use_local_subchannel_pool': 1 when constructing 
MessagingServiceClient
    - Give each RpcClient its own connection instead of grpc-js's shared global 
pool
    - Add broker-free regression test for cross-client isolation and 
intra-client reuse
---
 nodejs/src/client/RpcClient.ts       |   6 +-
 nodejs/test/client/RpcClient.test.ts | 150 +++++++++++++++++++++++++++++++++++
 2 files changed, 155 insertions(+), 1 deletion(-)

diff --git a/nodejs/src/client/RpcClient.ts b/nodejs/src/client/RpcClient.ts
index b485cc8d..78b15327 100644
--- a/nodejs/src/client/RpcClient.ts
+++ b/nodejs/src/client/RpcClient.ts
@@ -60,7 +60,11 @@ export class RpcClient {
   constructor(endpoints: Endpoints, sslEnabled: boolean) {
     const address = endpoints.getGrpcTarget();
     const grpcCredentials = sslEnabled ? ChannelCredentials.createSsl() : 
ChannelCredentials.createInsecure();
-    this.#client = new MessagingServiceClient(address, grpcCredentials);
+    // Use a local subchannel pool so each RpcClient owns its own connection
+    // instead of sharing one via grpc-js's global pool, aligning with the 
Java client.
+    this.#client = new MessagingServiceClient(address, grpcCredentials, {
+      'grpc.use_local_subchannel_pool': 1,
+    });
   }
 
   #getAndActivityRpcClient() {
diff --git a/nodejs/test/client/RpcClient.test.ts 
b/nodejs/test/client/RpcClient.test.ts
new file mode 100644
index 00000000..aae27e86
--- /dev/null
+++ b/nodejs/test/client/RpcClient.test.ts
@@ -0,0 +1,150 @@
+/**
+ * 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 { strict as assert } from 'node:assert';
+import {
+  Server,
+  ServerCredentials,
+  ServerDuplexStream,
+  ServerUnaryCall,
+  sendUnaryData,
+  Metadata,
+} from '@grpc/grpc-js';
+import { MessagingServiceService } from 
'../../proto/apache/rocketmq/v2/service_grpc_pb';
+import { TelemetryCommand, HeartbeatRequest, HeartbeatResponse } from 
'../../proto/apache/rocketmq/v2/service_pb';
+import { Code, Status } from '../../proto/apache/rocketmq/v2/definition_pb';
+import { RpcClient } from '../../src/client/RpcClient';
+import { Endpoints } from '../../src/route';
+
+async function waitForCount(getter: () => number, target: number, timeout = 
5000) {
+  const start = Date.now();
+  while (getter() < target) {
+    if (Date.now() - start > timeout) {
+      throw new Error(`timeout waiting for count ${target}, current 
${getter()}`);
+    }
+    await new Promise(resolve => setTimeout(resolve, 20));
+  }
+}
+
+describe('test/client/RpcClient.test.ts', () => {
+  let server: Server;
+  let port: number;
+  const telemetryPeers: string[] = [];
+  const heartbeatPeers: string[] = [];
+  const openStreams: ServerDuplexStream<TelemetryCommand, TelemetryCommand>[] 
= [];
+
+  before(async () => {
+    server = new Server();
+    server.addService(MessagingServiceService, {
+      telemetry(call: ServerDuplexStream<TelemetryCommand, TelemetryCommand>) {
+        telemetryPeers.push(call.getPeer());
+        openStreams.push(call);
+        // Keep the stream alive; just drain inbound data.
+        call.on('data', () => { /* noop */ });
+        call.on('error', () => { /* noop */ });
+        call.on('end', () => {
+          try {
+            call.end();
+          } catch {
+            // ignore
+          }
+        });
+      },
+      heartbeat(call: ServerUnaryCall<HeartbeatRequest, HeartbeatResponse>,
+        callback: sendUnaryData<HeartbeatResponse>) {
+        heartbeatPeers.push(call.getPeer());
+        const res = new HeartbeatResponse();
+        const status = new Status();
+        status.setCode(Code.OK);
+        res.setStatus(status);
+        callback(null, res);
+      },
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    } as any);
+    port = await new Promise<number>((resolve, reject) => {
+      server.bindAsync('127.0.0.1:0', ServerCredentials.createInsecure(), 
(err, boundPort) => {
+        if (err) return reject(err);
+        resolve(boundPort);
+      });
+    });
+  });
+
+  after(async () => {
+    for (const s of openStreams) {
+      try {
+        s.end();
+      } catch {
+        // ignore
+      }
+    }
+    await new Promise<void>(resolve => server.tryShutdown(() => resolve()));
+  });
+
+  it('should isolate gRPC connections between client instances (issue #1382)', 
async () => {
+    const base = telemetryPeers.length;
+    const endpoints = new Endpoints(`127.0.0.1:${port}`);
+    const client1 = new RpcClient(endpoints, false);
+    const client2 = new RpcClient(endpoints, false);
+
+    const stream1 = client1.telemetry(new Metadata());
+    stream1.on('error', () => { /* noop */ });
+    stream1.write(new TelemetryCommand());
+    await waitForCount(() => telemetryPeers.length, base + 1);
+
+    // Keep client1's stream alive while opening client2's stream.
+    const stream2 = client2.telemetry(new Metadata());
+    stream2.on('error', () => { /* noop */ });
+    stream2.write(new TelemetryCommand());
+    await waitForCount(() => telemetryPeers.length, base + 2);
+
+    const peer1 = telemetryPeers[base];
+    const peer2 = telemetryPeers[base + 1];
+
+    try {
+      assert.notEqual(peer1, peer2,
+        `two independent clients must not share a connection, but both used 
${peer1}`);
+    } finally {
+      stream1.destroy();
+      stream2.destroy();
+      client1.close();
+      client2.close();
+    }
+  });
+
+  it('should reuse one connection for telemetry and heartbeat within a single 
client', async () => {
+    const tBase = telemetryPeers.length;
+    const hBase = heartbeatPeers.length;
+    const endpoints = new Endpoints(`127.0.0.1:${port}`);
+    const client = new RpcClient(endpoints, false);
+
+    const stream = client.telemetry(new Metadata());
+    stream.on('error', () => { /* noop */ });
+    stream.write(new TelemetryCommand());
+    await waitForCount(() => telemetryPeers.length, tBase + 1);
+
+    await client.heartbeat(new HeartbeatRequest(), new Metadata(), 5000);
+    await waitForCount(() => heartbeatPeers.length, hBase + 1);
+
+    try {
+      assert.equal(telemetryPeers[tBase], heartbeatPeers[hBase],
+        'telemetry and heartbeat within one client must reuse the same 
connection');
+    } finally {
+      stream.destroy();
+      client.close();
+    }
+  });
+});

Reply via email to