BaseEncoding: Make encodingStream().close() idempotent#8183
BaseEncoding: Make encodingStream().close() idempotent#8183nickita-khylkouski wants to merge 1 commit intogoogle:masterfrom
Conversation
The OutputStream returned by encodingStream() violated the Closeable.close() contract: calling close() multiple times would write additional encoded data to the underlying Writer. This adds a boolean closed field and synchronized methods to track stream state, making close() idempotent and causing write()/flush() to throw IOException after close. Fixes google#5284
| public void write(int b) throws IOException { | ||
| public synchronized void write(int b) throws IOException { | ||
| if (closed) { | ||
| throw new IOException("Stream is closed"); |
There was a problem hiding this comment.
"Steam is closed" is repeated twice. Shouldn't that be defined as a reusable constant?
There was a problem hiding this comment.
The Guava pattern for this is a checkNotClosed() helper method, not a string constant — see AppendableWriter. Since this is a small anonymous class with only two call sites, I kept it inline, but happy to extract a helper if maintainers prefer.
|
|
||
| @Override | ||
| public void write(int b) throws IOException { | ||
| public synchronized void write(int b) throws IOException { |
There was a problem hiding this comment.
-
Synchronization is required for reliable communication between threads as well as for mutual exclusion.
-
Synchronization is not guaranteed to work unless both read and write operations are synchronized.
"Occasionally a program that synchronizes only writes (or reads) may appear to work on some machines, but in this case, appearances are deceiving."
--- Item 78 of Effective Java Programming by Joshua Blosch.
What justifies the need for the write of an abstract class to be synchronised without synchronising the read?
There was a problem hiding this comment.
All three methods that access the closed field — write(), flush(), and close() — are synchronized. Both the write to closed (in close()) and the reads of closed (in write() and flush()) happen within synchronized methods. There is no unsynchronized read.
This matches CharSequenceReader, which synchronizes every method. The synchronization was added per @cpovirk's guidance to ensure close() and write() cannot interleave.
Summary
boolean closedfield to theOutputStreamreturned byencodingStream()write(),flush(), andclose()synchronizedper @cpovirk's guidanceclose()returns early if already closed (idempotent)write()/flush()throwIOExceptionafter close, matchingAppendableWriterandCharSequenceReaderpatternsMotivation
BaseEncoding.encodingStream().close()violates theCloseable.close()contract which states: "If the stream is already closed then invoking this method has no effect."Calling
close()twice writes additional encoded data to the underlying Writer:This is problematic because double-close is common due to try-with-resources blocks and wrapping streams that close their underlying streams.
Fixes #5284
Testing
BaseEncodingTest:testEncodingStreamCloseIsIdempotent— verifies close() twice doesn't change outputtestEncodingStreamWriteAfterClose— verifies write() throws IOException after closetestEncodingStreamFlushAfterClose— verifies flush() throws IOException after closeBaseEncodingTestpass (./mvnw test -pl guava-tests -Dtest=BaseEncodingTest)IOExceptiontoReflectionFreeAssertThrowsto support the new test assertions