Conversation
There was a problem hiding this comment.
Can you review go/java-practices/null from the overall PR's perspective in terms of nullable vs optional ? I am not sure if we have a local java convention to prefer or avoid null
There was a problem hiding this comment.
We can't yet use java.util.Optional in many parts of the code because we don't require API desugaring and it was added in Android API level 24. We can use Guava's Optional, but we don't want it in any cross-module API surface; we only use Guava internally. Given Guava's Optional is semi-discouraged, but we can't use the new thing either, we've generally avoided the problem and not used Optional; there's very few usages of Optional in the code base.
Note that @Nullable is classically just documentation in gRPC, as we don't run a nullness checker and everything defaults to nullable. Although with tools re-interpreting the annotations with different JSpecify semantics (e.g., #12338), there's complications in its usage in cross-module API surfaces.
| private final int maxMessageSize; | ||
| private final int maxHeaderListSize; | ||
| private final int softLimitHeaderListSize; | ||
| private MetricRecorder metricRecorder; |
There was a problem hiding this comment.
How is this being set? I don't see a constructor or a setter that accesses this value?
| import java.util.Collections; | ||
| import java.util.List; | ||
|
|
||
| final class TcpMetrics { |
There was a problem hiding this comment.
optional: Javadocs for classes might be helpful
| } | ||
|
|
||
| static final class Metrics { | ||
| final LongCounterMetricInstrument connectionsCreated; |
There was a problem hiding this comment.
nullability annotations for fields where needed. Also consider the previous discussion about nullability style guide
| * Safe metric registration or retrieval for environments where TcpMetrics might | ||
| * be loaded multiple times (e.g., shaded and unshaded). | ||
| */ | ||
| private static LongCounterMetricInstrument safelyRegisterLongCounter( |
There was a problem hiding this comment.
Do we need the safelyRegi... private abstraction?
Given that all registration happens during construction and this class is finel , can't we simply ensure we dedupe our metrics during construction, catch and rethrow the error from the constructor? i.e. ensure that the input is valid and catch/throw from a single point which should never be reached.
The fact that "we are passing a valid set of metrics for registration" can probably be trivially unit tested when we construct a TcpMetrics instance.
This should reduce code bloat without any sacrifice to safety, but at the cost of "duplicate input will throw an error instead of being handled intuitively", which is a fair expectation IMO.
| java.util.List<String> labelValues = getLabelValues(channel); | ||
| try { | ||
| if (channel.getClass().getName().equals(epollSocketChannelClassName)) { | ||
| Class<?> tcpInfoClass = Class.forName(epollTcpInfoClassName); |
There was a problem hiding this comment.
This is quite a lot of reflection in hot exporting path. So, a couple of questions.
- Why do we choose reflectin over typecasting and calling actual methods? Reducing dependency surface?
- Is there a way to optimise this like caching value on per channel level? Something like a creating a runnable per channel. Not sure how it'd be possible to share this data for the final collection on channel inactive.
| Method rttMethod = tcpInfoClass.getMethod("rtt"); | ||
|
|
||
| long totalRetrans = (Long) totalRetransMethod.invoke(info); | ||
| int retransmits = (Integer) retransmitsMethod.invoke(info); |
There was a problem hiding this comment.
Are these cumulative metrics or are these total retransmits since the connection start? If latter, the current logic is incorrect.
There was a problem hiding this comment.
AAh I see, then I think we should not be using a counter for this... Will clarify in the gRFC
ejona86
left a comment
There was a problem hiding this comment.
"otel" is a very surprising prefix to use for the PR/commit, as opentelemetry isn't actually changed at all.
| @Nullable | ||
| public MetricRecorder getMetricRecorder() { | ||
| return metricRecorder; | ||
| } |
There was a problem hiding this comment.
LoadBalancer.Helper.getMetricRecorder() is non-null and ManagedChannelImpl creates a MetricRecorderImpl unconditionally. Make this non-null as well? I see that NameResolver allows it to be null; seems we should just change that. LoadBalancer used to be that way and it was changed after a bug in the Helper implementation caused the channel to panic.
If we need the optimization, we can have the MetricRecorder return whether a particular instrument is enabled. But I see no need to add an additional condition to usages for the metric recorder being missing entirely.
| this(metricRecorder, target, DEFAULT_METRICS); | ||
| } | ||
|
|
||
| Tracker(MetricRecorder metricRecorder, String target, Metrics metrics) { |
| "io.netty.channel.epoll.EpollTcpInfo"); | ||
| } | ||
|
|
||
| Tracker(MetricRecorder metricRecorder, String target, Metrics metrics, |
| private io.netty.util.concurrent.ScheduledFuture<?> reportTimer; | ||
|
|
||
| void channelActive(Channel channel) { | ||
| if (metricRecorder != null && target != null) { |
There was a problem hiding this comment.
When would the target be null, and if it is null why would we stop metrics? Seems we should worst-case use "" or something of the style <unknown target>. Otherwise you're silently losing metrics, which is the worst failure model for metrics as then you have lies.
| Collections.singletonList(target), labelValues); | ||
| } | ||
| } catch (Throwable t) { | ||
| // Epoll not available or error getting tcp_info, just ignore. |
There was a problem hiding this comment.
This comment only applies to a portion of the try block. Limit the scope of the try block to just where it is needed.
| return; | ||
| } | ||
| synchronized (accessorCache) { | ||
| channelReflectionAccessor = accessorCache.get(epollTcpInfoClassName); |
There was a problem hiding this comment.
Please no specialized cache just for testing. I'd suggest a single field and if epollTcpInfoClassName is provided for testing then simply create a new one (and don't cache it). And we can do all that in the constructor instead of checking every invocation. We do have some "interesting" tools that might help testing this (e.g., StaticTestingClassLoader), but I'll have to look over the tests to see how much help it provides.
…ddress PR comments)
The backoff timer is only used when serializeRetries=true, and that exists to match the old/current pick_first's behavior as closely as possible. InternalSubchannel.updateAddresses() would take no action when in TRANSIENT_FAILURE; it would update the addresses and just wait for the backoff timer to expire. Note that this only impacts serializeRetries=true; in the other cases we do want to start trying to the new addresses immediately, because the backoff timers are in the subchannels. Note that this change was also important because requestConnection() can be directly triggered by the user with channel.getState(true), and that shouldn't defeat the backoff timer.
Since we're only supporting API levels 23+, all the supported Android versions handle multidex natively, and without any bugs to workaround. Also bump some minSdkVersion that didn't get updated in fa7b52b so that multiDex is actually enabled by default. See also b/476359563
… and 'core: Wait for backoff timer on address update in pick_first'
ejona86
left a comment
There was a problem hiding this comment.
The PR description's "Implements A80" doesn't link to the gRFC; it just links to this PR.
| .setTransportTracerFactory(fakeClockTransportTracer) | ||
| .buildTransportServers(streamTracerFactories); | ||
| .setTransportTracerFactory(fakeClockTransportTracer); | ||
| return InternalOkHttpServerBuilder |
There was a problem hiding this comment.
You don't need to use InternalOkHttpServerBuilder; buildTransportServers() is package-private... the code was already calling it directly.
|
|
||
| @CanIgnoreReturnValue | ||
| @Override | ||
| public NettyServerBuilder addMetricSink(MetricSink metricSink) { |
There was a problem hiding this comment.
Looks like this method shouldn't have been created in this class, as it will be inherited from ForwardingServerBuilder.
Note that once this method is created, it can't be deleted, because previously-compiled code will be linking against this method that returns NettyServerBuilder, and if you delete it then the base class's method only returns ServerBuilder. (This is an ABI issue, not an API issue; recompiling doesn't see the issue.)
| * @param metricSink the metric sink to add. | ||
| * @return this | ||
| */ | ||
| public T addMetricSink(MetricSink metricSink) { |
There was a problem hiding this comment.
@ExperimentalApi. You can use "TODO" or similar as the string when developing, so just always add it immediately when creating a new API.
| private static List<Double> getMinRttBuckets() { | ||
| List<Double> buckets = new ArrayList<>(100); | ||
| for (int i = 1; i <= 100; i++) { | ||
| buckets.add(1e-6 * Math.pow(2.0, i * 0.24)); | ||
| } | ||
| return Collections.unmodifiableList(buckets); | ||
| } |
There was a problem hiding this comment.
This is pretty specific, and a lot of buckets. Where did this come from?
There was a problem hiding this comment.
I saw something similar in core...
There was a problem hiding this comment.
If implementations are expected to do this, then it needs to be in the gRFC.
| * @return this | ||
| */ | ||
| public T addMetricSink(MetricSink metricSink) { | ||
| throw new UnsupportedOperationException(); |
There was a problem hiding this comment.
If a server has no metrics, then they wouldn't implement this method. Seems this should just be a noop instead of throwing. (If it stays as throwing, we'd need to figure out how we wanted to handle it in GrpcOpenTelementry.)
| Attributes.EMPTY, | ||
| channelz); | ||
| channelz, | ||
| mock(MetricRecorder.class)); |
There was a problem hiding this comment.
Why is this one a mock, but the others in this same file aren't? This is the first mock used in the class, and there isn't any verification happening.
| return DEFAULT_METRICS; | ||
| } | ||
|
|
||
| static final class Metrics { |
There was a problem hiding this comment.
Is there any point to this class?
| boolean epollAvailable = false; | ||
| try { | ||
| Class<?> epollClass = Class.forName("io.netty.channel.epoll.Epoll"); | ||
| Method isAvailableMethod = epollClass.getDeclaredMethod("isAvailable"); | ||
| epollAvailable = (Boolean) isAvailableMethod.invoke(null); | ||
| } catch (ClassNotFoundException e) { | ||
| log.log(Level.FINE, "Epoll is not available", e); | ||
| } catch (Exception e) { | ||
| log.log(Level.FINE, "Failed to determine Epoll availability", e); | ||
| } catch (Error e) { | ||
| log.log(Level.FINE, "Failed to load native Epoll library", e); | ||
| } | ||
| log.log(Level.INFO, "Epoll available during static init of TcpMetrics:" | ||
| + "{0}", epollAvailable); |
| tcpInfo = epollTcpInfoClass.getDeclaredConstructor().newInstance(); | ||
| tcpInfoMethod = epollSocketChannelClass.getMethod("tcpInfo", epollTcpInfoClass); | ||
| totalRetransMethod = epollTcpInfoClass.getMethod("totalRetrans"); | ||
| retransMethod = epollTcpInfoClass.getMethod("retrans"); | ||
| rttMethod = epollTcpInfoClass.getMethod("rtt"); |
There was a problem hiding this comment.
We should avoid doing reflection every connection. These reflection results should be reused.
| totalRetransMethod = epollTcpInfoClass.getMethod("totalRetrans"); | ||
| retransMethod = epollTcpInfoClass.getMethod("retrans"); | ||
| rttMethod = epollTcpInfoClass.getMethod("rtt"); | ||
| } catch (Exception | Error t) { |
There was a problem hiding this comment.
Please use precise error handling. What specific exceptions are expected here?
Implements A80