Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(JenkinsClient): use new HTTP client with better diagnostics #656

Merged
merged 5 commits into from
Aug 20, 2021

Conversation

keturn
Copy link
Member

@keturn keturn commented Aug 18, 2021

I was trying to review another PR and I noticed none of the preview releases were showing up. One of our Jenkins-as-repository / HTTPS / redirect sorts of issues. So I tried swapping out the JenkinsClient#openStream implementation for one that used the newer HttpClient from Java 11.

We do get more control of how redirects are handled and more visibility in to the HTTP response if needed for troubleshooting.

Looks to be successfully following the redirects too.

It is also awfully verbose for this simple use case.

then I spent like two hours going through mailing list archives looking for the part where someone explained why HttpRequest takes a URI, not a URL. Surely that must have been a FAQ while it was going through its development and incubator phase, right? But I haven't found an answer yet, or even anyone on the net-dev list raising the question.

We get more control of how redirects are handled and more visibility in to the HTTP response if needed for troubleshooting.
@keturn
Copy link
Member Author

keturn commented Aug 18, 2021

This is the failing test:

@Test
@DisplayName("should handle IOException on request(url) gracefully")
void nullOnIoException() throws IOException, InterruptedException {
final Gson gson = new Gson();
final JenkinsClient client = new JenkinsClient(gson);
URL urlThrowingException = mock(URL.class);
doThrow(IOException.class).when(urlThrowingException).openConnection();
assertNull(client.request(urlThrowingException));
}

which uses mockito to throw an exception when the given URL's openConnection method is called.

But HttpClient never calls url.openConnection.

I guess I should make that test override client#openStream instead.

@keturn
Copy link
Member Author

keturn commented Aug 18, 2021

I then discovered this results in JenkinsClient being able to follow redirects to get the properties file, but then DownloadUtils isn't able to download the corresponding release. 🤦

This tries to be a minimal change from the previous version, but the mixture of streaming APIs used is not pretty.
Copy link
Member Author

@keturn keturn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:octocat:

@keturn
Copy link
Member Author

keturn commented Aug 19, 2021

InterruptedException

I'm reluctant to catch InterruptedException because it feels like a sign that something is trying to shut everything down. How far should we let it bubble up?

Or maybe this is the wrong interpretation. Because it's not the current thread that's been interrupted—that was some reader thread that we were waiting on. Maybe that should be considered an implementation detail and it should be wrapped up in a DownloadException like most anything else.

HttpClient

We don't really want to be creating a new HttpClient instance for every request. Especially since we tend to make a lot of requests against the same host. It'd be nice to move that to some Service or other singleton. That would let us move those HttpClient configuration lines out of the request methods too.

It's not a huge deal, but we could probably shave off some time from the big stack of requests JenkinsClient does to get a displayVersion for every build.

DownloadTask & DownloadUtils.downloadToFile

In the name of incremental progress (and because this rabbit hole was already far enough from where I started out), I tried to leave most of the existing DownloadUtils stuff intact and mostly just swap out the part responsible for getting the InputStream started.

But it's all a jumble of concurrency and stream models. I was thinking downloadToFile should return a Task, instead of requiring ProgressListener to be passed in and not making any use of its return value.

then I find out we already have a DownloadTask extends Task class.
and there's this asynchronous progress reporting, but the code calling downloadToFile really expects it to block until completion.

It wouldn't be a huge project to rewire, but more than I wanted to bite off without checking for scope creep.

There's also the part where Task is JavaFX-specific, and that work would be for the purpose of supporting the JavaFX progress bar, and @skaldarnar has been mumbling about throwing out JavaFX entirely, so I don't know if it's a good time to invest in JavaFX-specific code.

instead of relying on the URL argument to be used in a specific way
@keturn keturn marked this pull request as ready for review August 19, 2021 02:17
@keturn
Copy link
Member Author

keturn commented Aug 19, 2021

I almost got to write that without mockito, but "static declarations in inner classes" are not supported until language level 16.

anyway, test is fixed now, so taking this out of Draft status.

Copy link
Member

@skaldarnar skaldarnar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like those changes 👍

Feel free to either merge straight away or address the review comments - nothing blocking here from my side.

@@ -79,7 +80,7 @@ public void install(GameRelease release, ProgressListener listener) throws IOExc
}
}

private void download(GameRelease release, Path targetLocation, ProgressListener listener) throws DownloadException, IOException {
private void download(GameRelease release, Path targetLocation, ProgressListener listener) throws DownloadException, IOException, InterruptedException {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm also not sure about the InterruptedException. I would have thought that this is similar to any other exception that could cause the download to fail...

However, I do think this should be pretty rare, and unless we get a bunch of complaints from users we should be good either way.

URLConnection connection = url.openConnection();
connection.setConnectTimeout(10 * 1000);
return connection.getInputStream();
var client = HttpClient.newBuilder()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add a //TODO: ... to reuse one client for multiple requests?

connection.setConnectTimeout(10 * 1000);
return connection.getInputStream();
var client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

.followRedirects(HttpClient.Redirect.NORMAL)
.build();
var request = HttpRequest.newBuilder(url.toURI()).build();
var response = client.send(request, HttpResponse.BodyHandlers.ofInputStream());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure I like those vars too much ... 🤔 When building a client or request it's clear to me what the type most likely is, but I'm less sure what the response actually is. Sure, my IDE does show it to, but unfortunately Github does not 😕

I don't think we should be as strict as the presto community, as I think that var does have it's place for local fields in many places...
I probably just need to get used to them 😅

@@ -73,8 +82,8 @@ Properties requestProperties(final URL artifactUrl) {
final Properties properties = new Properties();
properties.load(inputStream);
return properties;
} catch (IOException e) {
e.printStackTrace();
} catch (IOException | URISyntaxException | InterruptedException e) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we catch the InterruptedException here but not in the other places?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, because it was used in Optional.map, and the type of thing Optional.map takes cannot throw checked exceptions

listener.update(0);

final HttpURLConnection connection = getConnectedDownloadConnection(downloadURL);
var result = getConnectedDownloadConnection(downloadURL);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, I either want an explicit type here, or a better name for getConnectedDownloadConnection 🙈

Comment on lines +39 to +40
try (var mockedClientClass = mockStatic(JenkinsClient.class)) {
mockedClientClass.when(() -> JenkinsClient.openStream(any())).thenThrow(IOException.class);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, we mock only the static part of JenkinsClient here, right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just double-checked and I see docs saying "Represents an active mock of a type's static methods," so yes.

@keturn keturn merged commit 3a1bc1c into master Aug 20, 2021
@keturn keturn deleted the chore/httpClient branch August 20, 2021 01:25
@keturn
Copy link
Member Author

keturn commented Aug 21, 2021

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants