Skip to content

Use try-with-resources to ensure streams are closed#710

Open
oliverlockwood wants to merge 6 commits intodrewnoakes:mainfrom
dalet-oss:use-try-with-resources-to-ensure-streams-are-closed
Open

Use try-with-resources to ensure streams are closed#710
oliverlockwood wants to merge 6 commits intodrewnoakes:mainfrom
dalet-oss:use-try-with-resources-to-ensure-streams-are-closed

Conversation

@oliverlockwood
Copy link

This replaces the clumsily-raised PR #662 (the delivery of which I apologise for on behalf of my now ex-colleague!) - please close it now.

We had been working with a local build due to time pressures, and unfortunately Nick failed to isolate the functional changes (for submission upstream) from the local changes relating to publication of the artifact at our own co-ordinates.

The context of this PR is as follows:

  • During production operation of one of our applications, we have experienced some memory leaks.
  • One source of memory leaks was identified as unclosed streams.
  • We conducted a review of our codebase and dependencies, including this library.
  • The essence of the PR is to use try-with-resources, as Java best practices advise, when creating instances of classes that implement the java.lang.AutoCloseable interface (implicitly including those that implement java.io.Closeable, also).

Hopefully now with context clear and noise reduced, this should be fairly quick and easy for you to review.

Many thanks for your time and maintenance of the library.

Copy link
Owner

@drewnoakes drewnoakes left a comment

Choose a reason for hiding this comment

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

Thank you for resurrecting this PR with more targeted changes.

In reviewing these, I'm wondering which of these changes is actually addressing a memory leak. I can only see one of the several changes here that, to my understanding, should have a positive impact at run time (and even then I'm not sure), which is the closing of InflaterInputStream.

There are a few places that introduce a change in behaviour, where the new code will close a stream that's passed as a parameter. This doesn't feel very idiomatic in Java, and I'm reluctant to take those changes. The general philosophy seems to be if you create a stream, you close it. So the close should be handled by the caller. This allows a caller to re-use the stream if they need to, perhaps by seeking or performing further reads beyond where the called method reached.

There's no need to close ByteArrayInputStream or ByteArrayOutputStream. I'm unclear on BufferedInputStream but suspect that most (all?) implementations would avoid using unmanaged resources.

Adding try/finally blocks to methods that didn't previously have them can cause the JIT to be more conservative in its code generation, so I like to avoid these when they don't add anything.

tl;dr can you share where you saw the leaks you mentioned please?

Comment on lines 180 to 181
try {
InflaterInputStream inflateStream = new InflaterInputStream(new ByteArrayInputStream(compressedProfile));
try (ByteArrayInputStream bais = new ByteArrayInputStream(compressedProfile);
InflaterInputStream inflateStream = new InflaterInputStream(bais)) {
Copy link
Owner

Choose a reason for hiding this comment

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

Looks good for the inflate stream, but closing ByteArrayInputStream has no effect, per the docs. Not a big deal to close both though.

Copy link
Contributor

Choose a reason for hiding this comment

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

Closing a ByteArrayInputStream does nothing, but many static code analysis tools will complain about any AutoClosable that isn't managed by try-with-resources, so I'd say that's a pretty standard way to do it. But, you shouldn't need to "split" the stream creation, it should be possible to use try (new InflaterInputStream(new ByteArrayInputStream(compressedProfile))). The assumption is that the outer stream will always close the inner stream when closed.

Copy link
Author

Choose a reason for hiding this comment

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

Thanks for the feedback - I will inline the bais variable both here and in other cases, below.

Comment on lines 135 to 136
try (BufferedInputStream bufferedInputStream = inputStream instanceof BufferedInputStream
? (BufferedInputStream)inputStream : new BufferedInputStream(inputStream)) {
Copy link
Owner

Choose a reason for hiding this comment

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

I don't think this method should be taking ownership of the inputStream parameter. The caller may wish to use it after calling this method. My sense is that the changes to this file should be undone.

public static byte[] readAllBytes(InputStream stream) throws IOException
{
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
Copy link
Owner

Choose a reason for hiding this comment

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

Closing ByteArrayOutputStream does nothing. The changes in this file add overhead and can interfere with JIT compilation due to the addition of try/finally. I'd revert the changes to this file.

Comment on lines 84 to 90
BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
try (InputStreamReader inputStreamReader = new InputStreamReader(stream);
BufferedReader reader = new BufferedReader(inputStreamReader)) {
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
return sb.toString();
}
return sb.toString();
Copy link
Owner

Choose a reason for hiding this comment

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

This will close the provided stream. I don't think the changes in this file add anything other than overhead.

// Extract information from embedded image since it is metadata-rich
ByteArrayInputStream jpegmem = new ByteArrayInputStream(jpegrawbytes);
try {
try (ByteArrayInputStream jpegmem = new ByteArrayInputStream(jpegrawbytes)) {
Copy link
Author

Choose a reason for hiding this comment

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

I'm inclined to leave this change in because although as you point out, closing a BAIS has no effect, we already have a try block so it's zero overhead to do so (and actually saves a line of code).

But if you want me to revert this change, please say so.

…g multiple stream initialisers in try-with-resources; removing try-with-resources for BAIS/BAOS where it just adds noise
@oliverlockwood
Copy link
Author

Hi @drewnoakes , @Nadahar - thank you for the feedback. Apologies for the delay in response - I was on holiday and then ill for a few days.

  • In terms of your review feedback, I believe I have now addressed it all throughout - both where explicitly specified, and also similar instances of the same that appeared elsewhere in the PR
  • Regarding your question of "where did we see the leaks" - unfortunately, I can't identify that, because this was not documented, shared or handed over when we my ex-colleague departed the company.

Hopefully despite this, the PR in its simplified form will be useful to you, us, and others.

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.

3 participants