Harmonize @Nullable annotations in okhttp instrumentation - #7375
Conversation
This is a just firefighting and not a proper fix. The proper fix would be non-null request and nullable response but fixing that seems to be a very deep rabbit hole and also involves breaking changes. Closes micrometer-metricsgh-7373
shakuzen
left a comment
There was a problem hiding this comment.
Looks like the right fix to me. I'm not sure why we didn't do this to begin with and why it wasn't caught by errorprone. I don't think we can know the Request is not null unless we check because it is marked @Nullable on the CallState, and the person who reported the issue apparently ran into it being null in some case.
There was a problem hiding this comment.
@jonatan-ivanov Thanks a lot for the quick reply and the PR! I reviewed it as best as I could, but obviously you are way more experienced in this codebase, so please disregard it if I'm wrong.
Meanwhile I will try to reproduce this better in isolation.
the person who reported the issue apparently ran into [the request] being null in some case.
@shakuzen FWIW, I actually only observed the Response being null. The Request was not.
Sorry, the original issue should've made that more clear.
| } | ||
|
|
||
| private String getUriTag(Function<Request, String> urlMapper, @Nullable Request request) { | ||
| private String getUriTag(Function<@Nullable Request, String> urlMapper, @Nullable Request request) { |
There was a problem hiding this comment.
Taking into account the if condition on the following line, I believe the urlMapper can remain untouched. AFAICT there are no unchecked usages of OkhttpContext.getUrlMapper in the micrometer codebase, so it should be safe to keep the public API in this case.
There was a problem hiding this comment.
You are right (this is the only place when it is called). I synchronized it with BiFunction because leaving them different might make things worse for the users when they pass them to the ctor of OkHttpContext.
OkHttpContext(
Function<@Nullable Request, String> urlMapper,
...,
Iterable<BiFunction<@Nullable Request, @Nullable Response, KeyValue>> contextSpecificTags,
...)I found it confusing to leave Request non-null for the Function but nullable on BiFunction but not sure which one is better for the future (I want to properly fix this).
| private final Iterable<Tag> extraTags; | ||
|
|
||
| private final Iterable<BiFunction<Request, Response, Tag>> contextSpecificTags; | ||
| private final Iterable<BiFunction<@Nullable Request, @Nullable Response, Tag>> contextSpecificTags; |
There was a problem hiding this comment.
I went through the codebase this morning with a fresh mind, so if I understand correctly the whole rabbit hole of nullability boils down to the fact that the public API OkHttpContext does not override the nullable SenderContext.getCarrier
I understand that changing the context is not really possible without widely affecting public API, but I believe this doesn't apply to OkHttpMetricsEventListener, which is completely isolated from that context (and pretty isolated in terms of public API, too). In this class, I think we can fully determine nullability and only need <_, @Nullable Response, _>:
- The only entry-point is
callStart. Here,Requestis guaranteed not null,Responseis guaranteed null - Other lifecycle callbacks do not add new entries to the shared
callStatemap, and do not alter any entry'sRequest. So, its still guaranteed not null responseHeadersEndpotentially updates theResponse
Since CallState is private API, we know these are all relevant places, so we should be fine to drop the @Nullable annotation from CallState.Request
There was a problem hiding this comment.
I understand that changing the context is not really possible without widely affecting public API,
I think I might be able to fix things by just breaking this:
public void setState(OkHttpObservationInterceptor.CallState state) {
this.state = state;
}
public OkHttpObservationInterceptor.@Nullable CallState getState() {
return state;
}These are public methods but CallState is package-private so users are not able to use it anyways. (Though I don't like the parameters of the OkHttpContext either, they should be on the convention I think and that's really a breaking change.)
I believe this doesn't apply to OkHttpMetricsEventListener, which is completely isolated from that context (and pretty isolated in terms of public API, too). In this class, I think we can fully determine nullability and only need <_, @nullable Response, _>
I applied the same thought-process here as in #7375 (comment), to keep things consistent(ly wrong).
There was a problem hiding this comment.
These are public methods but
CallStateis package-private so users are not able to use it anyways.
I wonder if there is some static analysis or ErrorProne capability we could use to prevent adding such broken public API to begin with (public API that requires non-public types). I'm fine breaking that in 1.17.0-RC1 since external users can't use the API anyway, right?
There was a problem hiding this comment.
They can do context.setState(null) and/or Object obj = context.getState() but neither makes a lot of sense.
I haven't seen any errorprone check for this: https://www.jetbrains.com/help/inspectopedia/ClassEscapesDefinedScope.html#locating-this-inspection
Maybe we should open an issue and/or check if we can use spotbugs or pmd for this.
|
Existing tests actually already cover the failing scenario, they are just missing the assertions which kotlin would insert. This will cause a failure: diff --git a/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/okhttp3/OkHttpMetricsEventListenerTest.java b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/okhttp3/OkHttpMetricsEventListenerTest.java
index 3e6809089..5f9268805 100644
--- a/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/okhttp3/OkHttpMetricsEventListenerTest.java
+++ b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/okhttp3/OkHttpMetricsEventListenerTest.java
@@ -100,6 +100,11 @@ class OkHttpMetricsEventListenerTest {
OkHttpClient client = new OkHttpClient.Builder().connectTimeout(1, TimeUnit.MILLISECONDS)
.eventListener(OkHttpMetricsEventListener.builder(registry, "okhttp.requests")
+ .tag((req, res) -> {
+ assertThat(req).isNotNull();
+ assertThat(res).isNotNull();
+ return Tag.of("key", "asdf");
+ })
.tags(Tags.of("foo", "bar"))
.uriMapper(URI_MAPPER)
.build())I can also confirm that updating the annotations will really produce the expected behaviour when using kotlin (using the exact piece of code that triggered it before): Bytecode with
|
Not in this PR, but I would get rid of the |
shakuzen
left a comment
There was a problem hiding this comment.
Changes look good for 1.16.x to me. We can do more in main, I think.
|
Fyi, I needed to fix tests after merge: 55b8f06 Tests were ok locally for me so I did not even followed them through, NullAway should have screamed that my changes broke tests. The PR build was also fine. But after merge, this change broke the build. The failure is valid, the tests did not follow the implementation so I fixed them separately. Right now I have no idea why this did not fail locally and (more importantly) on the PR build. |
This is a just firefighting and not a proper fix.
The proper fix would be non-null request and nullable response but fixing that seems to be a very deep rabbit hole and also involves breaking changes.
Closes gh-7373