0

Here is a JUnit test that demonstrates my issue:

package stream;

import static org.junit.jupiter.api.Assertions.*;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

import org.junit.jupiter.api.Test;

class StreamTest {
    public static class LoopbackStream {
        private final byte[] END_MARKER = new byte[0];
        private final ArrayBlockingQueue<byte[]> queue = new ArrayBlockingQueue<>(1024);

        public OutputStream getOutputStream() {
            return new OutputStream() {
                @Override
                public void write(int b) throws IOException {
                    this.write(new byte[] { (byte) b });
                }

                @Override
                public void write(byte[] b, int off, int len) {
                    try {
                        queue.put(Arrays.copyOfRange(b, off, len - off));
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                }

                @Override
                public void close() {
                    try {
                        queue.put(END_MARKER);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                }
            };
        }

        public InputStream getInputStream() {
            return new InputStream() {
                private boolean finished = false;
                private ByteBuffer current = ByteBuffer.wrap(new byte[0]);

                @Override
                public int read() {
                    if (ensureData()) {
                        return Byte.toUnsignedInt(current.get());
                    } else {
                        return -1;
                    }
                }

                @Override
                public int read(byte[] b, int off, int len) {
                    if (ensureData()) {
                        int position = current.position();
                        current.get(b, off, Math.min(len, current.remaining()));
                        return current.position() - position;
                    } else {
                        return -1;
                    }
                }

                private boolean ensureData() {
                    if (!finished && !current.hasRemaining()) {
                        try {
                            byte[] data = queue.take();
                            current = ByteBuffer.wrap(data);
                            finished = data == END_MARKER;
                        } catch (InterruptedException e) {
                            Thread.currentThread().interrupt();
                            return false;
                        }
                    }
                    return !finished;
                }
            };
        }

    }

    @Test
    void testVanilla() throws IOException {
        LoopbackStream objectUnderTest = new LoopbackStream();

        PrintWriter pw = new PrintWriter(new OutputStreamWriter(objectUnderTest.getOutputStream()), true);
        BufferedReader br = new BufferedReader(new InputStreamReader(objectUnderTest.getInputStream()));

        pw.println("Hello World!");

        assertEquals("Hello World!", br.readLine());
    }

    @Test
    void testVanilla2() throws IOException {
        LoopbackStream objectUnderTest = new LoopbackStream();

        PrintWriter pw = new PrintWriter(new OutputStreamWriter(objectUnderTest.getOutputStream()), true);
        BufferedReader br = new BufferedReader(new InputStreamReader(objectUnderTest.getInputStream()));

        pw.println("Hello World!");
        assertEquals("Hello World!", br.readLine());

        pw.println("Hello Otherworld!");
        assertEquals("Hello Otherworld!", br.readLine());
    }

    @Test
    void testGzipped() throws IOException {
        LoopbackStream objectUnderTest = new LoopbackStream();

        PrintWriter pw = new PrintWriter(new OutputStreamWriter(new GZIPOutputStream(objectUnderTest.getOutputStream(), true)), true);
        BufferedReader br = new BufferedReader(new InputStreamReader(new GZIPInputStream(objectUnderTest.getInputStream())));

        pw.println("Hello World!");

        assertEquals("Hello World!", br.readLine());
    }
}

There are two individual tests. One that uses vanilla input and output streams (which works fine) and another that wraps those streams in their gzip equivalents.

I've used the GZIPOutputStream's syncFlush option which I am expecting to automatically flush any remaining bytes from the stream whenever the parent stream is flushed. I'm using the PrintWriter's autoFlush option to flush its data whenever it does a println.

Is there a better way to force the GZIPOutputStream to flush its buffers after a println?

spierepf
  • 2,774
  • 2
  • 30
  • 52

1 Answers1

1

I know that this is not the full answer to your question, but it is too long for a comment...


Update:

After further investigation it seems that it's not the GZIPOutputStream that doesn't flush (by adding System.out.println("xy"); statements in the public void write(byte[] b, int off, int len) method you can see that the GZIPOutputStream writes two byte arrays into your OutputStream: one is the gzip stream header, the other one is the encoded content of the first line of text).

It seems that the reading process blocks because of a bad interaction between the java.io.InputStreamReader (respectively the sun.nio.cs.StreamDecoder it uses) and the GZIPInputStream.

Basically, if the StreamDecoder needs to read bytes from the underlying stream it tries to read as many bytes as possible (as long as underlying stream reports in.available() > 0, implying that the underlying stream can yield some more bytes without blocking)

The problem with this check is that the InflaterInputStream (the superclass of the GZIPInputStream) always returns 1 for the number of available bytes, even if its source stream has no bytes available (see the source of InflaterInputStream.available())

So it seems that while you can write line by line into a GZIPOutputStream, you cannot easily read line by line from a GZIPInputStream...


Original answer:

The problem is not the GZIPOutputStream, the problem is with the boolean ensureData() method that refuses to read more than one block.

The following test fails with vanilla streams too:

@Test
void testVanilla2() throws IOException {
    LoopbackStream objectUnderTest = new LoopbackStream();

    PrintWriter pw = new PrintWriter(new OutputStreamWriter(objectUnderTest.getOutputStream()), true);
    BufferedReader br = new BufferedReader(new InputStreamReader(objectUnderTest.getInputStream()));

    pw.println("Hello World!");
    assertEquals("Hello World!", br.readLine());

    pw.println("Hello Otherworld!");
    assertEquals("Hello Otherworld!", br.readLine());
}

Thomas Kläger
  • 17,754
  • 3
  • 23
  • 34
  • You're right, there was a bug in my LoopbackStream class. Thanks! Fixing the bug doesn't fix my actual problem however, so I've updated the code in my original question. – spierepf Mar 24 '21 at 01:52
  • @spierepf I've updated the answer with my findings from `GZIPOutputStream` / `GZIPInputStream` – Thomas Kläger Mar 24 '21 at 22:05