5

I'm using OpenJDK 11 on Linux and I need to make sure all my web requests done with HttpURLConnection are properly closed and do not keep any file descriptors open.

Oracle's manual tells to use close on the InputStream and Android's manual tells to use disconnect on the HttpURLConnection object.

I also set Connection: close and http.keepAlive to false to avoid pooling of connections.

This seems to work with plain http requests but not encrypted https requests whose response is sent with non-chunked encoding. Only a GC seems to clean up the closed connections.

This example code:

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.stream.Stream;

public class Test {
    private static int printFds() throws IOException {
        int cnt = 0;
        try (Stream<Path> paths = Files.list(new File("/proc/self/fd").toPath())) {
            for (Path path : (Iterable<Path>)paths::iterator) {
                System.out.println(path);
                ++cnt;
            }
        }
        System.out.println();
        return cnt;
    }

    public static void main(String[] args) throws IOException, InterruptedException {
        System.setProperty("http.keepAlive", "false");
        for (int i = 0; i < 10; i++) {
            // Must be a https endpoint returning non-chunked response
            HttpURLConnection conn = (HttpURLConnection) new URL("https://www.google.com/").openConnection();
            conn.setRequestProperty("Connection", "close");
            BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
            while (in.readLine() != null) {
            }
            in.close();
            conn.disconnect();
            conn = null;
            in = null;
        }

        Thread.sleep(1000);
        int numBeforeGc = printFds();

        System.gc();
        Thread.sleep(1000);
        int numAfterGc = printFds();
        System.out.println(numBeforeGc == numAfterGc ? "No socket leaks" : "Sockets were leaked");
    }
}

prints this output:

/proc/self/fd/0
/proc/self/fd/1
/proc/self/fd/2
/proc/self/fd/3
/proc/self/fd/4
/proc/self/fd/5
/proc/self/fd/9
/proc/self/fd/6
/proc/self/fd/7
/proc/self/fd/8
/proc/self/fd/10
/proc/self/fd/11
/proc/self/fd/12
/proc/self/fd/13
/proc/self/fd/14
/proc/self/fd/15
/proc/self/fd/16
/proc/self/fd/17
/proc/self/fd/18
/proc/self/fd/19

/proc/self/fd/0
/proc/self/fd/1
/proc/self/fd/2
/proc/self/fd/3
/proc/self/fd/4
/proc/self/fd/5
/proc/self/fd/9
/proc/self/fd/6
/proc/self/fd/7
/proc/self/fd/8

Sockets were leaked

Changing to a http URL makes the sockets close correctly as expected without GC:

/proc/self/fd/0
/proc/self/fd/1
/proc/self/fd/2
/proc/self/fd/3
/proc/self/fd/4
/proc/self/fd/5
/proc/self/fd/6

/proc/self/fd/0
/proc/self/fd/1
/proc/self/fd/2
/proc/self/fd/3
/proc/self/fd/4
/proc/self/fd/5
/proc/self/fd/6

No socket leak

Tested with both OpenJDK 11 and 12. Did I miss something or is this a bug?

Emil
  • 16,784
  • 2
  • 41
  • 52
  • `System#gc` has no obligation to perform a garbage collection; it only serves as a hint for the GC to use. See: [Why is it bad practice to call System.gc()?](https://stackoverflow.com/questions/2414105/why-is-it-bad-practice-to-call-system-gc) – Jacob G. Aug 09 '19 at 20:11
  • @Jacob G. The explicit `System.gc()` is just to illustrate the issue to show that it's the GC that cleans the sockets and not my `close()` and `disconnect()` calls. – Emil Aug 09 '19 at 20:14
  • maybe it's not closing because it's waiting for a response from the other side. (though it wouldn't explain the GC). Try doing a ~5 minutes sleep, perhaps 1 second isn't enough. – Tomer Aug 09 '19 at 21:25
  • With netstat I see CLOSE_WAIT, so the other side has sent FIN. – Emil Aug 09 '19 at 22:14
  • 1
    I tried 5 minute sleep and there was no difference. By running strace, I see `shutdown(fd, SHUT_RD)`, but no `close` (until GC). – Emil Aug 09 '19 at 22:24
  • interesting! how about you try first disconnect and then close? – Tomer Aug 09 '19 at 22:43
  • @Tomer that gives the same result. – Emil Aug 09 '19 at 22:45
  • BTW: are you omitting URLConnection.connect() on purpose? (checked the API and it should be called). In addition, try closing the output stream as well. The API states: "Invoking the close() methods on the InputStream or OutputStream of an URLConnection after a request may free network resources associated with this instance, unless particular protocol specifications specify different behaviours for it." – Tomer Aug 09 '19 at 22:51
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/197748/discussion-between-tomer-and-emil). – Tomer Aug 09 '19 at 22:55
  • The `connect()` call is not present in Oracle's and Android's examples. However I tried adding it before `getInputStream()` but didn't make any difference. – Emil Aug 09 '19 at 22:55
  • Interestingly, it isn’t just the socket. The HttpURLConnection itself is not being garbage collected. – VGR Aug 09 '19 at 23:43
  • @Tomer Keep reading. It is called automatically when you get an input or output or error stream. And disconnect before close makes no sense. – user207421 Aug 10 '19 at 00:10
  • Can you try a longer sleep? You seem to be still getting connection pooling at your end, although I would have expected a lot of connection reuse rather than new connections. I can't look it up now but is there an `https.keepAlive` system property? – user207421 Aug 10 '19 at 00:16
  • I don't think it pools the connections since strace tells that it calls shutdown immediately when the input stream has been read to its end. I think openjdk just forgets to also call close. I also tried setting https.keepAlive to false but no difference. – Emil Aug 10 '19 at 00:21
  • I don't know why it calls shutdown. There is no shutdown in either HTTP or TLS, and in any case shutting down the input of a socket that is already at end of stream is totally redundant. Sounds like a bug to me. – user207421 Aug 10 '19 at 01:12
  • TLS 1.3 allows half-closed connections, so shutdown is valid for generic TLS streams, but I don't know why you would do it for HTTPS. – Emil Aug 10 '19 at 07:04

1 Answers1

1

Turns out to be a bug after all: https://bugs.openjdk.java.net/browse/JDK-8216326

shutdownInput is now replaced by close in the latest builds of JDK 11 and 13 (but not 12).

Emil
  • 16,784
  • 2
  • 41
  • 52