Skip to content

Commit 1fd2eed

Browse files
committed
feat: support long link
1 parent 0b9b7e2 commit 1fd2eed

18 files changed

Lines changed: 983 additions & 98 deletions

File tree

trpc-core/src/main/java/com/tencent/trpc/core/cluster/RpcClusterClientManager.java

Lines changed: 41 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,15 @@
4444
* scanner closes idle connections.</li>
4545
* <li>A lightweight background timer periodically (every
4646
* {@value #RECONNECT_CHECK_PERIOD_SECONDS}s) scans cached clients. If a client is found
47-
* to be unavailable, its failure counter is incremented; once the counter reaches
48-
* {@value #MAX_RECONNECT_FAILURES}, the client is closed and evicted, freeing memory and
49-
* allowing the next request to rebuild a fresh long connection.</li>
50-
* <li>When the underlying {@link RpcClient} closes itself (transport error or our own timer),
51-
* the {@link RpcClient#closeFuture()} callback removes the cache entry.</li>
47+
* to be unavailable, its failure counter is incremented and a warning is logged. The
48+
* scanner <b>never actively closes</b> the underlying transport: doing so would tear
49+
* down its Netty {@code EventLoopGroup} (and any in-flight long-connection requests).
50+
* Recovery is delegated to the transport's existing lazy reconnect, which is triggered
51+
* by the request path ({@link com.tencent.trpc.core.transport.AbstractClientTransport
52+
* #ensureChannelActive}) or by Netty's {@code channelInactive} event.</li>
53+
* <li>When the underlying {@link RpcClient} closes itself (transport error or explicit
54+
* shutdown), the {@link RpcClient#closeFuture()} callback removes the cache entry so
55+
* the next request rebuilds a fresh long connection.</li>
5256
* <li>{@link #shutdownBackendConfig(BackendConfig)} / {@link #close()} still release clients
5357
* explicitly.</li>
5458
* </ul>
@@ -62,8 +66,9 @@ public class RpcClusterClientManager {
6266
*/
6367
private static final int RECONNECT_CHECK_PERIOD_SECONDS = 30;
6468
/**
65-
* Maximum number of consecutive reconnect-check failures tolerated before the client is
66-
* closed and evicted from the cache.
69+
* Maximum number of consecutive reconnect-check failures tolerated before a warning is
70+
* escalated. The scanner will <b>not</b> close the client transport itself; recovery is
71+
* delegated to the transport's lazy reconnect.
6772
*/
6873
private static final int MAX_RECONNECT_FAILURES = 5;
6974

@@ -180,12 +185,13 @@ private static void ensureReconnectCheckerStarted() {
180185

181186
/**
182187
* Periodic check: walk every cached client; for each one
183-
* that is currently unavailable, increment its failure counter; once the counter reaches
184-
* {@link #MAX_RECONNECT_FAILURES} the client is closed (which triggers
185-
* {@code closeFuture} → cache eviction). Healthy clients have their counter reset.
186-
* <p>The check itself does not actively send a heartbeat: it simply observes the current
187-
* connection state. The transport's existing lazy reconnect (triggered by request path or
188-
* by Netty's channelInactive event) takes care of re-establishing the long connection.</p>
188+
* that is currently unavailable, increment its failure counter and log a warning once the
189+
* counter reaches {@link #MAX_RECONNECT_FAILURES}. Healthy clients have their counter reset.
190+
* <p>The check itself does not actively send a heartbeat and <b>never closes the underlying
191+
* transport</b>: closing would tear down the shared Netty {@code EventLoopGroup} and abort
192+
* in-flight long-connection requests. The transport's existing lazy reconnect (triggered by
193+
* request path or by Netty's {@code channelInactive} event) takes care of re-establishing
194+
* the long connection.</p>
189195
*/
190196
static void checkAndReconnect() {
191197
if (CLOSED_FLAG.get()) {
@@ -195,24 +201,29 @@ static void checkAndReconnect() {
195201
try {
196202
if (proxy.isAvailable()) {
197203
proxy.failureCount.set(0);
204+
proxy.warnedAtMaxFailures.set(false);
198205
return;
199206
}
200-
int fails = proxy.failureCount.incrementAndGet();
207+
// Cap the counter at MAX_RECONNECT_FAILURES to avoid unbounded growth (and the
208+
// resulting integer wrap-around) when the backend stays unavailable for a very
209+
// long time. Once capped, further iterations are silent — the warning has
210+
// already been emitted at the threshold-crossing iteration below.
211+
int fails = proxy.failureCount.updateAndGet(
212+
cur -> cur >= MAX_RECONNECT_FAILURES ? MAX_RECONNECT_FAILURES : cur + 1);
201213
if (logger.isDebugEnabled()) {
202214
logger.debug("Reconnect-check: client {} not available, failureCount={}",
203215
proxy.getProtocolConfig().toSimpleString(), fails);
204216
}
205-
if (fails >= MAX_RECONNECT_FAILURES) {
217+
if (fails == MAX_RECONNECT_FAILURES
218+
&& proxy.warnedAtMaxFailures.compareAndSet(false, true)) {
219+
// Log only once per unavailability spell to avoid log spam. The flag is
220+
// reset as soon as the proxy becomes available again (see the healthy
221+
// branch above is reached via a separate state, so we reset there too).
206222
logger.warn("Reconnect-check: client {} unavailable for {} consecutive checks "
207-
+ "(~{}s), closing and evicting from cache",
223+
+ "(~{}s); leaving the transport intact and relying on lazy "
224+
+ "reconnect on the request path",
208225
proxy.getProtocolConfig().toSimpleString(), fails,
209226
fails * RECONNECT_CHECK_PERIOD_SECONDS);
210-
try {
211-
proxy.close();
212-
} catch (Throwable ex) {
213-
logger.error("Close stale client {} failed",
214-
proxy.getProtocolConfig().toSimpleString(), ex);
215-
}
216227
}
217228
} catch (Throwable ex) {
218229
logger.error("Reconnect-check on client {} threw", key, ex);
@@ -308,9 +319,16 @@ private static class RpcClientProxy implements RpcClient {
308319
private final RpcClient delegate;
309320
/**
310321
* Consecutive failure count observed by the reconnect-check timer; reset to 0 whenever
311-
* the client is observed available.
322+
* the client is observed available. Capped at {@link #MAX_RECONNECT_FAILURES} to avoid
323+
* unbounded growth.
312324
*/
313325
final AtomicInteger failureCount = new AtomicInteger(0);
326+
/**
327+
* Whether a "stuck unavailable" warning has already been emitted for the current
328+
* unavailability spell. Reset to {@code false} as soon as the proxy is observed
329+
* available again, so a future incident produces a fresh warning.
330+
*/
331+
final AtomicBoolean warnedAtMaxFailures = new AtomicBoolean(false);
314332

315333
RpcClientProxy(RpcClient delegate) {
316334
this.delegate = delegate;

trpc-core/src/main/java/com/tencent/trpc/core/common/Constants.java

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,11 @@ public class Constants {
3636

3737
public static final String DEFAULT_KEEP_ALIVE = "true";
3838
/**
39-
* Default shared IO thread pool true
39+
* Default shared IO thread pool: true. The shared pool now supports both NIO and
40+
* Epoll: each transport joins one of two reference-counted shared groups based on
41+
* {@code Epoll.isAvailable() && config.useEpoll()}, so enabling epoll no longer forces
42+
* the user to also flip {@code ioThreadGroupShare} off. Server-side transports do not
43+
* consult this flag (they always own their own group).
4044
*/
4145
public static final String DEFAULT_IO_THREAD_GROUPSHARE = "true";
4246
/**
@@ -119,9 +123,34 @@ public class Constants {
119123
*/
120124
public static final String DEFAULT_BUFFER_SIZE = "16384";
121125
/**
122-
* Default client idle timeout 3 minutes
126+
* Default client idle timeout 3 minutes. Drives the read-idle close handler installed
127+
* by {@code NettyTcpClientTransport}: a long connection that has not received any
128+
* inbound bytes for this duration is recycled by the client (the next request goes
129+
* through lazy reconnect). Half-dead detection in faster paths is delegated to the
130+
* configurable TCP keepalive parameters below; the read-idle handler is the slow
131+
* universal fallback that also covers macOS / Windows where TCP keepalive tuning is
132+
* not available.
123133
*/
124134
public static final String DEFAULT_IDLE_TIMEOUT = "180000";
135+
/**
136+
* Default TCP keepalive idle (Linux {@code TCP_KEEPIDLE}) in seconds: 30 s without
137+
* traffic on the socket before the kernel starts emitting keepalive probes. Together
138+
* with {@link #DEFAULT_TCP_KEEPALIVE_INTVL} and {@link #DEFAULT_TCP_KEEPALIVE_CNT}
139+
* (Dubbo-style 30/10/3) this yields a half-dead detection window of roughly 60 s on
140+
* Linux + epoll (30 + 10 × 3 = 60). Value 0 leaves the OS default (Linux: 7200 s).
141+
*/
142+
public static final String DEFAULT_TCP_KEEPALIVE_IDLE = "30";
143+
/**
144+
* Default TCP keepalive interval (Linux {@code TCP_KEEPINTVL}) in seconds between
145+
* consecutive keepalive probes after the idle window has elapsed.
146+
*/
147+
public static final String DEFAULT_TCP_KEEPALIVE_INTVL = "10";
148+
/**
149+
* Default TCP keepalive probe count (Linux {@code TCP_KEEPCNT}): the number of
150+
* unacknowledged keepalive probes after which the kernel marks the connection dead
151+
* and emits a RST.
152+
*/
153+
public static final String DEFAULT_TCP_KEEPALIVE_CNT = "3";
125154
/**
126155
* Default server idle timeout 4 minutes
127156
*/

trpc-core/src/main/java/com/tencent/trpc/core/common/config/BackendConfig.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,9 @@ public ProtocolConfig generateProtocolConfig(String host, int port, String netwo
497497
config.setPort(port);
498498
config.setNetwork(network);
499499
config.setIdleTimeout(idleTimeout);
500+
config.setTcpKeepAliveIdle(tcpKeepAliveIdle);
501+
config.setTcpKeepAliveIntvl(tcpKeepAliveIntvl);
502+
config.setTcpKeepAliveCnt(tcpKeepAliveCnt);
500503
config.setCharset(charset);
501504
config.setLazyinit(lazyinit);
502505
config.setConnsPerAddr(connsPerAddr);

trpc-core/src/main/java/com/tencent/trpc/core/common/config/BaseProtocolConfig.java

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,28 @@ public class BaseProtocolConfig implements Serializable, Cloneable {
9494
*/
9595
@ConfigProperty(value = Constants.DEFAULT_IDLE_TIMEOUT, type = Integer.class, override = true, moreZero = false)
9696
protected Integer idleTimeout;
97+
/**
98+
* TCP keepalive idle in seconds (Linux {@code TCP_KEEPIDLE}). Applied only on
99+
* Linux + epoll TCP client transports. Value 0 disables tuning and leaves the OS
100+
* default in place (Linux ≈ 7200 s).
101+
*/
102+
@ConfigProperty(value = Constants.DEFAULT_TCP_KEEPALIVE_IDLE, type = Integer.class, override = true,
103+
moreZero = false)
104+
protected Integer tcpKeepAliveIdle;
105+
/**
106+
* TCP keepalive probe interval in seconds (Linux {@code TCP_KEEPINTVL}). Applied only
107+
* on Linux + epoll TCP client transports. Value 0 disables tuning.
108+
*/
109+
@ConfigProperty(value = Constants.DEFAULT_TCP_KEEPALIVE_INTVL, type = Integer.class, override = true,
110+
moreZero = false)
111+
protected Integer tcpKeepAliveIntvl;
112+
/**
113+
* TCP keepalive probe count (Linux {@code TCP_KEEPCNT}). Applied only on Linux +
114+
* epoll TCP client transports. Value 0 disables tuning.
115+
*/
116+
@ConfigProperty(value = Constants.DEFAULT_TCP_KEEPALIVE_CNT, type = Integer.class, override = true,
117+
moreZero = false)
118+
protected Integer tcpKeepAliveCnt;
97119
/**
98120
* Whether to delay initialization.
99121
*/
@@ -303,6 +325,33 @@ public void setIdleTimeout(Integer idleTimeout) {
303325
this.idleTimeout = idleTimeout;
304326
}
305327

328+
public Integer getTcpKeepAliveIdle() {
329+
return tcpKeepAliveIdle;
330+
}
331+
332+
public void setTcpKeepAliveIdle(Integer tcpKeepAliveIdle) {
333+
checkFiledModifyPrivilege();
334+
this.tcpKeepAliveIdle = tcpKeepAliveIdle;
335+
}
336+
337+
public Integer getTcpKeepAliveIntvl() {
338+
return tcpKeepAliveIntvl;
339+
}
340+
341+
public void setTcpKeepAliveIntvl(Integer tcpKeepAliveIntvl) {
342+
checkFiledModifyPrivilege();
343+
this.tcpKeepAliveIntvl = tcpKeepAliveIntvl;
344+
}
345+
346+
public Integer getTcpKeepAliveCnt() {
347+
return tcpKeepAliveCnt;
348+
}
349+
350+
public void setTcpKeepAliveCnt(Integer tcpKeepAliveCnt) {
351+
checkFiledModifyPrivilege();
352+
this.tcpKeepAliveCnt = tcpKeepAliveCnt;
353+
}
354+
306355
public Boolean getLazyinit() {
307356
return lazyinit;
308357
}

trpc-core/src/main/java/com/tencent/trpc/core/transport/AbstractClientTransport.java

Lines changed: 79 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -257,21 +257,93 @@ protected void ensureChannelActive(int chIndex) {
257257
ChannelFutureItem curChannelItem = channels.get(chIndex);
258258
// initiate a new connection creation action when the connection is not yet established or the connection
259259
// is broken
260-
boolean futureIsDoneAndNotConnected =
261-
!curChannelItem.isAvailable() && !curChannelItem.isConnecting();
262-
// not available and not establishing a connection
263-
if (futureIsDoneAndNotConnected || curChannelItem.isNotYetConnect()) {
260+
if (!needsReconnect(curChannelItem)) {
261+
return;
262+
}
263+
connLock.lock();
264+
try {
265+
// Double-check inside the lock: another thread may have already replaced this slot
266+
// with a fresh ChannelFutureItem (either still connecting or already connected).
267+
// Without this check, a thundering-herd of requests arriving right after a
268+
// disconnect would each rebuild the slot, producing a connect/disconnect storm
269+
// against the peer and a burst of short-lived TIME_WAIT sockets.
270+
ChannelFutureItem latest = channels.get(chIndex);
271+
if (!needsReconnect(latest)) {
272+
return;
273+
}
274+
channels.set(chIndex, new ChannelFutureItem(createChannel().toCompletableFuture(), config));
275+
try {
276+
latest.close();
277+
} catch (Exception ex) {
278+
logger.error("close " + latest + " exception", ex);
279+
}
280+
} finally {
281+
connLock.unlock();
282+
}
283+
}
284+
285+
/**
286+
* A slot needs a fresh connection when it is either uninitialized (lazy-init not yet
287+
* triggered) or its previous future has finished without producing a connected channel.
288+
* A slot whose future is still in flight ({@code isConnecting()}) is left alone — the
289+
* in-flight {@code bootstrap.connect} will publish the result into the same item.
290+
*/
291+
private static boolean needsReconnect(ChannelFutureItem item) {
292+
if (item.isNotYetConnect()) {
293+
return true;
294+
}
295+
return !item.isAvailable() && !item.isConnecting();
296+
}
297+
298+
/**
299+
* Invalidate the slot holding {@code target}: replace it with a blank placeholder
300+
* ({@code channelFuture==null}, i.e. {@code isNotYetConnect=true}) so the next request
301+
* unconditionally goes through {@link #ensureChannelActive} and rebuilds a fresh
302+
* connection. The previous item is closed best-effort.
303+
* <p>Called by the client-side idle handler so that <em>before</em> the actual
304+
* {@code channel.close()} runs (asynchronously in the EventLoop), the request thread
305+
* already sees the slot as needing a reconnect — eliminating the "request lands on a
306+
* channel that is about to be closed" race window.</p>
307+
*/
308+
@Override
309+
public void invalidateChannel(Channel target) {
310+
if (target == null || channels == null || channels.isEmpty()) {
311+
return;
312+
}
313+
// The list is bounded by connsPerAddr; a linear scan is fine.
314+
for (int i = 0; i < channels.size(); i++) {
315+
ChannelFutureItem item = channels.get(i);
316+
if (item == null || item.channelFuture == null || !item.channelFuture.isDone()
317+
|| item.channelFuture.isCompletedExceptionally()) {
318+
continue;
319+
}
320+
Channel ch;
321+
try {
322+
ch = item.channelFuture.join();
323+
} catch (Throwable ignore) {
324+
continue;
325+
}
326+
if (ch != target) {
327+
continue;
328+
}
264329
connLock.lock();
265330
try {
266-
channels.set(chIndex, new ChannelFutureItem(createChannel().toCompletableFuture(), config));
331+
// Re-read under the lock to avoid clobbering a slot another thread already
332+
// refreshed (e.g. concurrent reconnect).
333+
ChannelFutureItem latest = channels.get(i);
334+
if (latest != item) {
335+
return;
336+
}
337+
channels.set(i, new ChannelFutureItem(null, config));
267338
try {
268-
curChannelItem.close();
339+
item.close();
269340
} catch (Exception ex) {
270-
logger.error("close " + curChannelItem + " exception", ex);
341+
logger.error("close invalidated " + item + " exception", ex);
271342
}
272343
} finally {
273344
connLock.unlock();
274345
}
346+
return;
275347
}
276348
}
277349

trpc-core/src/main/java/com/tencent/trpc/core/transport/ClientTransport.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,22 @@ public interface ClientTransport {
6262
*/
6363
ChannelHandler getChannelHandler();
6464

65+
/**
66+
* Mark the slot currently holding {@code channel} as invalidated so that the very next
67+
* request observes "needs reconnect" instead of routing onto the about-to-be-closed
68+
* channel. The default no-op keeps the contract optional for transports that do not
69+
* support a slot/pool model.
70+
* <p>This is the hook used by client-side idle handlers: when the idle handler decides to
71+
* tear down a long-idle channel, it calls {@code invalidateChannel} <b>before</b>
72+
* {@code channel.close()} so that the request-thread side cannot read the stale slot any
73+
* longer. The actual TCP close happens asynchronously in the EventLoop afterwards.</p>
74+
*
75+
* @param channel the channel about to be closed; may be {@code null} (no-op).
76+
*/
77+
default void invalidateChannel(Channel channel) {
78+
// default no-op
79+
}
80+
6581
/**
6682
* Get the remote address.
6783
*/

0 commit comments

Comments
 (0)