Skip to content
Open
Show file tree
Hide file tree
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
142 changes: 142 additions & 0 deletions netty/src/main/java/io/grpc/netty/EpollTcpInfoAccessor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/*
* Copyright 2026 The gRPC Authors
*
* Licensed 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.
*/

package io.grpc.netty;

import com.google.common.annotations.VisibleForTesting;
import io.netty.channel.Channel;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.EnumMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nullable;

/**
* Reflective access to {@code EpollSocketChannel.tcpInfo()}, since grpc-netty does not depend on
* netty-epoll at compile time.
*/
final class EpollTcpInfoAccessor {
private static final Logger log = Logger.getLogger(EpollTcpInfoAccessor.class.getName());

@Nullable
static final EpollTcpInfoAccessor INSTANCE = load();

enum Field {
STATE("state"),
CA_STATE("caState"),
RETRANSMITS("retransmits"),
PROBES("probes"),
BACKOFF("backoff"),
OPTIONS("options"),
SND_WSCALE("sndWscale"),
RCV_WSCALE("rcvWscale"),
RTO("rto"),
ATO("ato"),
SND_MSS("sndMss"),
RCV_MSS("rcvMss"),
UNACKED("unacked"),
SACKED("sacked"),
LOST("lost"),
RETRANS("retrans"),
FACKETS("fackets"),
LAST_DATA_SENT("lastDataSent"),
LAST_ACK_SENT("lastAckSent"),
LAST_DATA_RECV("lastDataRecv"),
LAST_ACK_RECV("lastAckRecv"),
PMTU("pmtu"),
RCV_SSTHRESH("rcvSsthresh"),
RTT("rtt"),
RTTVAR("rttvar"),
SND_SSTHRESH("sndSsthresh"),
SND_CWND("sndCwnd"),
ADVMSS("advmss"),
REORDERING("reordering"),
TOTAL_RETRANS("totalRetrans");

final String methodName;

Field(String methodName) {
this.methodName = methodName;
}
}

private final Class<?> channelClass;
private final Constructor<?> infoConstructor;
private final Method tcpInfo;
private final EnumMap<Field, Method> getters;

private EpollTcpInfoAccessor(
Class<?> channelClass, Constructor<?> infoConstructor, Method tcpInfo,
EnumMap<Field, Method> getters) {
this.channelClass = channelClass;
this.infoConstructor = infoConstructor;
this.tcpInfo = tcpInfo;
this.getters = getters;
}

@Nullable
private static EpollTcpInfoAccessor load() {
boolean epollAvailable = false;
try {
Class<?> epollClass = Class.forName("io.netty.channel.epoll.Epoll");
Method isAvailableMethod = epollClass.getDeclaredMethod("isAvailable");
epollAvailable = (Boolean) isAvailableMethod.invoke(null);
if (epollAvailable) {
return create(
Class.forName("io.netty.channel.epoll.EpollSocketChannel"),
Class.forName("io.netty.channel.epoll.EpollTcpInfo"));
}
} catch (ReflectiveOperationException e) {
log.log(Level.FINE, "Failed to initialize Epoll tcp_info reflection", e);
} finally {
log.log(Level.FINE, "Epoll available during static init of EpollTcpInfoAccessor:"
+ "{0}", epollAvailable);
}
return null;
}

@VisibleForTesting
static EpollTcpInfoAccessor create(Class<?> channelClass, Class<?> infoClass)
throws ReflectiveOperationException {
EnumMap<Field, Method> getters = new EnumMap<>(Field.class);
for (Field field : Field.values()) {
getters.put(field, infoClass.getMethod(field.methodName));
}
return new EpollTcpInfoAccessor(
channelClass,
infoClass.getDeclaredConstructor(),
channelClass.getMethod("tcpInfo", infoClass),
getters);
}

boolean isEpollSocketChannel(Channel channel) {
return channelClass.isInstance(channel);
}

Object newTcpInfo() throws ReflectiveOperationException {
return infoConstructor.newInstance();
}

/** Fills {@code info} with the current {@code TCP_INFO} of {@code channel}. */
void read(Channel channel, Object info) throws ReflectiveOperationException {
tcpInfo.invoke(channel, info);
}

long get(Object info, Field field) throws ReflectiveOperationException {
return ((Number) getters.get(field).invoke(info)).longValue();
}
}
72 changes: 67 additions & 5 deletions netty/src/main/java/io/grpc/netty/NettySocketSupport.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,25 @@

package io.grpc.netty;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import io.grpc.InternalChannelz.TcpInfo;
import io.grpc.netty.EpollTcpInfoAccessor.Field;
import io.netty.channel.Channel;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nullable;

/**
* An class for getting low level socket info.
*/
final class NettySocketSupport {
private static volatile Helper instance = new NettySocketHelperImpl();
private static final Logger log = Logger.getLogger(NettySocketSupport.class.getName());

private static volatile Helper instance =
new NettySocketHelperImpl(EpollTcpInfoAccessor.INSTANCE);

interface Helper {
/**
Expand Down Expand Up @@ -63,12 +70,67 @@ static void setHelper(Helper helper) {
instance = Preconditions.checkNotNull(helper);
}

private static final class NettySocketHelperImpl implements Helper {
@VisibleForTesting
static final class NettySocketHelperImpl implements Helper {
@Nullable
private final EpollTcpInfoAccessor epollInfo;

NettySocketHelperImpl(@Nullable EpollTcpInfoAccessor epollInfo) {
this.epollInfo = epollInfo;
}

@Override
@Nullable
public NativeSocketOptions getNativeSocketOptions(Channel ch) {
// TODO(zpencer): if netty-epoll, use reflection to call EpollSocketChannel.tcpInfo()
// And/or if some other low level socket support library is available, call it now.
return null;
if (epollInfo == null || !epollInfo.isEpollSocketChannel(ch)) {
return null;
}
TcpInfo tcpInfo;
try {
tcpInfo = readTcpInfo(epollInfo, ch);
} catch (ReflectiveOperationException e) {
log.log(Level.FINE, "Failed to read Epoll tcp_info", e);
return null;
}
return new NativeSocketOptions(tcpInfo, ImmutableMap.<String, String>of());
}

// Channelz fields are uint32, so narrowing the epoll values to int keeps their bits.
private static TcpInfo readTcpInfo(EpollTcpInfoAccessor epollInfo, Channel ch)
throws ReflectiveOperationException {
Object info = epollInfo.newTcpInfo();
epollInfo.read(ch, info);
return new TcpInfo.Builder()
.setState((int) epollInfo.get(info, Field.STATE))
.setCaState((int) epollInfo.get(info, Field.CA_STATE))
.setRetransmits((int) epollInfo.get(info, Field.RETRANSMITS))
.setProbes((int) epollInfo.get(info, Field.PROBES))
.setBackoff((int) epollInfo.get(info, Field.BACKOFF))
.setOptions((int) epollInfo.get(info, Field.OPTIONS))
.setSndWscale((int) epollInfo.get(info, Field.SND_WSCALE))
.setRcvWscale((int) epollInfo.get(info, Field.RCV_WSCALE))
.setRto((int) epollInfo.get(info, Field.RTO))
.setAto((int) epollInfo.get(info, Field.ATO))
.setSndMss((int) epollInfo.get(info, Field.SND_MSS))
.setRcvMss((int) epollInfo.get(info, Field.RCV_MSS))
.setUnacked((int) epollInfo.get(info, Field.UNACKED))
.setSacked((int) epollInfo.get(info, Field.SACKED))
.setLost((int) epollInfo.get(info, Field.LOST))
.setRetrans((int) epollInfo.get(info, Field.RETRANS))
.setFackets((int) epollInfo.get(info, Field.FACKETS))
.setLastDataSent((int) epollInfo.get(info, Field.LAST_DATA_SENT))
.setLastAckSent((int) epollInfo.get(info, Field.LAST_ACK_SENT))
.setLastDataRecv((int) epollInfo.get(info, Field.LAST_DATA_RECV))
.setLastAckRecv((int) epollInfo.get(info, Field.LAST_ACK_RECV))
.setPmtu((int) epollInfo.get(info, Field.PMTU))
.setRcvSsthresh((int) epollInfo.get(info, Field.RCV_SSTHRESH))
.setRtt((int) epollInfo.get(info, Field.RTT))
.setRttvar((int) epollInfo.get(info, Field.RTTVAR))
.setSndSsthresh((int) epollInfo.get(info, Field.SND_SSTHRESH))
.setSndCwnd((int) epollInfo.get(info, Field.SND_CWND))
.setAdvmss((int) epollInfo.get(info, Field.ADVMSS))
.setReordering((int) epollInfo.get(info, Field.REORDERING))
.build();
}
}
}
69 changes: 9 additions & 60 deletions netty/src/main/java/io/grpc/netty/TcpMetrics.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
import io.grpc.MetricRecorder;
import io.netty.channel.Channel;
import io.netty.util.concurrent.ScheduledFuture;
import java.lang.reflect.Method;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.Arrays;
Expand All @@ -38,57 +37,7 @@
final class TcpMetrics {
private static final Logger log = Logger.getLogger(TcpMetrics.class.getName());

static EpollInfo epollInfo = loadEpollInfo();

static final class EpollInfo {
final Class<?> channelClass;
final java.lang.reflect.Constructor<?> infoConstructor;
final Method tcpInfo;
final Method totalRetrans;
final Method retransmits;
final Method rtt;

EpollInfo(
Class<?> channelClass,
java.lang.reflect.Constructor<?> infoConstructor,
Method tcpInfo,
Method totalRetrans,
Method retransmits,
Method rtt) {
this.channelClass = channelClass;
this.infoConstructor = infoConstructor;
this.tcpInfo = tcpInfo;
this.totalRetrans = totalRetrans;
this.retransmits = retransmits;
this.rtt = rtt;
}
}

static EpollInfo loadEpollInfo() {
boolean epollAvailable = false;
try {
Class<?> epollClass = Class.forName("io.netty.channel.epoll.Epoll");
Method isAvailableMethod = epollClass.getDeclaredMethod("isAvailable");
epollAvailable = (Boolean) isAvailableMethod.invoke(null);
if (epollAvailable) {
Class<?> channelClass = Class.forName("io.netty.channel.epoll.EpollSocketChannel");
Class<?> infoClass = Class.forName("io.netty.channel.epoll.EpollTcpInfo");
return new EpollInfo(
channelClass,
infoClass.getDeclaredConstructor(),
channelClass.getMethod("tcpInfo", infoClass),
infoClass.getMethod("totalRetrans"),
infoClass.getMethod("retrans"),
infoClass.getMethod("rtt"));
}
} catch (ReflectiveOperationException e) {
log.log(Level.FINE, "Failed to initialize Epoll tcp_info reflection", e);
} finally {
log.log(Level.FINE, "Epoll available during static init of TcpMetrics:"
+ "{0}", epollAvailable);
}
return null;
}
static EpollTcpInfoAccessor epollInfo = EpollTcpInfoAccessor.INSTANCE;

private static final long RECORD_INTERVAL_MILLIS = TimeUnit.MINUTES.toMillis(5);
private final MetricRecorder metricRecorder;
Expand All @@ -102,7 +51,7 @@ static EpollInfo loadEpollInfo() {
Object tcpInfo = null;
if (epollInfo != null) {
try {
tcpInfo = epollInfo.infoConstructor.newInstance();
tcpInfo = epollInfo.newTcpInfo();
} catch (ReflectiveOperationException e) {
log.log(Level.FINE, "Failed to instantiate EpollTcpInfo", e);
}
Expand All @@ -120,7 +69,7 @@ void channelActive(Channel channel) {
}

private void scheduleNextReport(final Channel channel, boolean isInitial) {
if (epollInfo == null || !epollInfo.channelClass.isInstance(channel) || !channel.isActive()) {
if (epollInfo == null || !epollInfo.isEpollSocketChannel(channel) || !channel.isActive()) {
return;
}

Expand All @@ -147,7 +96,7 @@ void channelInactive(Channel channel) {
metricRecorder.addLongUpDownCounter(InternalTcpMetrics.CONNECTION_COUNT_INSTRUMENT, -1,
Collections.emptyList(), labelValues);
// Final collection on close
if (epollInfo != null && epollInfo.channelClass.isInstance(channel)) {
if (epollInfo != null && epollInfo.isEpollSocketChannel(channel)) {
recordTcpInfo(channel, true);
}
}
Expand All @@ -157,18 +106,18 @@ void recordTcpInfo(Channel channel) {
}

private void recordTcpInfo(Channel channel, boolean isClose) {
if (epollInfo == null || !epollInfo.channelClass.isInstance(channel)) {
if (epollInfo == null || !epollInfo.isEpollSocketChannel(channel)) {
return;
}
List<String> labelValues = getLabelValues(channel);
long totalRetrans;
long retransmits;
long rtt;
try {
epollInfo.tcpInfo.invoke(channel, tcpInfo);
totalRetrans = (Long) epollInfo.totalRetrans.invoke(tcpInfo);
retransmits = (Long) epollInfo.retransmits.invoke(tcpInfo);
rtt = (Long) epollInfo.rtt.invoke(tcpInfo);
epollInfo.read(channel, tcpInfo);
totalRetrans = epollInfo.get(tcpInfo, EpollTcpInfoAccessor.Field.TOTAL_RETRANS);
retransmits = epollInfo.get(tcpInfo, EpollTcpInfoAccessor.Field.RETRANSMITS);
rtt = epollInfo.get(tcpInfo, EpollTcpInfoAccessor.Field.RTT);
} catch (ReflectiveOperationException e) {
log.log(Level.FINE, "Error computing TCP metrics", e);
return;
Expand Down
Loading
Loading