Skip to content

Commit 1425dd9

Browse files
authored
fix: update BaseStorageReadChannel to be left open unless explicitly closed (#1853)
Add two new tests to verify new expected close behavior surfaced from java-storage-nio.
1 parent 4491f73 commit 1425dd9

File tree

2 files changed

+84
-18
lines changed

2 files changed

+84
-18
lines changed

google-cloud-storage/src/main/java/com/google/cloud/storage/BaseStorageReadChannel.java

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,12 @@
2222
import com.google.cloud.storage.BufferedReadableByteChannelSession.BufferedReadableByteChannel;
2323
import java.io.IOException;
2424
import java.nio.ByteBuffer;
25+
import java.nio.channels.ClosedChannelException;
2526
import org.checkerframework.checker.nullness.qual.Nullable;
2627

2728
abstract class BaseStorageReadChannel<T> implements StorageReadChannel {
2829

30+
private boolean open;
2931
private ByteRangeSpec byteRangeSpec;
3032
private int chunkSize = _2MiB;
3133
private BufferHandle bufferHandle;
@@ -34,6 +36,7 @@ abstract class BaseStorageReadChannel<T> implements StorageReadChannel {
3436
@Nullable private T resolvedObject;
3537

3638
protected BaseStorageReadChannel() {
39+
this.open = true;
3740
this.byteRangeSpec = ByteRangeSpec.nullRange();
3841
}
3942

@@ -45,16 +48,12 @@ public final synchronized void setChunkSize(int chunkSize) {
4548

4649
@Override
4750
public final synchronized boolean isOpen() {
48-
if (lazyReadChannel == null) {
49-
return true;
50-
} else {
51-
LazyReadChannel<T> tmp = internalGetLazyChannel();
52-
return tmp.isOpen();
53-
}
51+
return open;
5452
}
5553

5654
@Override
5755
public final synchronized void close() {
56+
open = false;
5857
if (internalGetLazyChannel().isOpen()) {
5958
StorageException.wrapIOException(internalGetLazyChannel().getChannel()::close);
6059
}
@@ -75,17 +74,23 @@ public final ByteRangeSpec getByteRangeSpec() {
7574

7675
@Override
7776
public final synchronized int read(ByteBuffer dst) throws IOException {
77+
// BlobReadChannel only considered itself closed if close had been called on it.
78+
if (!open) {
79+
throw new ClosedChannelException();
80+
}
7881
long diff = byteRangeSpec.length();
7982
if (diff <= 0) {
80-
close();
8183
return -1;
8284
}
8385
try {
84-
int read = internalGetLazyChannel().getChannel().read(dst);
86+
// trap if the fact that tmp is already closed, and instead return -1
87+
BufferedReadableByteChannel tmp = internalGetLazyChannel().getChannel();
88+
if (!tmp.isOpen()) {
89+
return -1;
90+
}
91+
int read = tmp.read(dst);
8592
if (read != -1) {
8693
byteRangeSpec = byteRangeSpec.withShiftBeginOffset(read);
87-
} else {
88-
close();
8994
}
9095
return read;
9196
} catch (StorageException e) {
@@ -128,15 +133,16 @@ protected void setResolvedObject(@Nullable T resolvedObject) {
128133
protected abstract LazyReadChannel<T> newLazyReadChannel();
129134

130135
private void maybeResetChannel(boolean freeBuffer) throws IOException {
131-
if (lazyReadChannel != null && lazyReadChannel.isOpen()) {
132-
try (BufferedReadableByteChannel ignore = lazyReadChannel.getChannel()) {
133-
if (bufferHandle != null && !freeBuffer) {
134-
bufferHandle.get().clear();
135-
} else if (freeBuffer) {
136-
bufferHandle = null;
137-
}
138-
lazyReadChannel = null;
136+
if (lazyReadChannel != null) {
137+
if (lazyReadChannel.isOpen()) {
138+
lazyReadChannel.getChannel().close();
139+
}
140+
if (bufferHandle != null && !freeBuffer) {
141+
bufferHandle.get().clear();
142+
} else if (freeBuffer) {
143+
bufferHandle = null;
139144
}
145+
lazyReadChannel = null;
140146
}
141147
}
142148

google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBlobReadChannelTest.java

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,13 @@
1616

1717
package com.google.cloud.storage.it;
1818

19+
import static com.google.cloud.storage.TestUtils.assertAll;
1920
import static com.google.cloud.storage.TestUtils.xxd;
2021
import static com.google.common.truth.Truth.assertThat;
2122
import static java.nio.charset.StandardCharsets.UTF_8;
2223
import static org.junit.Assert.assertArrayEquals;
2324
import static org.junit.Assert.assertNotNull;
25+
import static org.junit.Assert.assertThrows;
2426
import static org.junit.Assert.fail;
2527

2628
import com.google.cloud.ReadChannel;
@@ -52,6 +54,7 @@
5254
import java.io.IOException;
5355
import java.nio.ByteBuffer;
5456
import java.nio.channels.Channels;
57+
import java.nio.channels.ClosedChannelException;
5558
import java.nio.channels.FileChannel;
5659
import java.nio.channels.WritableByteChannel;
5760
import java.nio.file.Files;
@@ -372,6 +375,40 @@ public void seekAfterReadWorks() throws IOException {
372375
}
373376
}
374377

378+
@Test
379+
public void seekBackToStartAfterReachingEndOfObjectWorks() throws IOException {
380+
ObjectAndContent obj512KiB = objectsFixture.getObj512KiB();
381+
BlobInfo gen1 = obj512KiB.getInfo();
382+
byte[] bytes = obj512KiB.getContent().getBytes();
383+
384+
int from = bytes.length - 5;
385+
byte[] expected1 = Arrays.copyOfRange(bytes, from, bytes.length);
386+
387+
String xxdExpected1 = xxd(expected1);
388+
String xxdExpected2 = xxd(bytes);
389+
try (ReadChannel reader = storage.reader(gen1.getBlobId())) {
390+
// seek forward to a new offset
391+
reader.seek(from);
392+
393+
try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
394+
WritableByteChannel out = Channels.newChannel(baos)) {
395+
ByteStreams.copy(reader, out);
396+
String xxd = xxd(baos.toByteArray());
397+
assertThat(xxd).isEqualTo(xxdExpected1);
398+
}
399+
400+
// seek back to the beginning
401+
reader.seek(0);
402+
// read again
403+
try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
404+
WritableByteChannel out = Channels.newChannel(baos)) {
405+
ByteStreams.copy(reader, out);
406+
String xxd = xxd(baos.toByteArray());
407+
assertThat(xxd).isEqualTo(xxdExpected2);
408+
}
409+
}
410+
}
411+
375412
@Test
376413
public void limitAfterReadWorks() throws IOException {
377414
ObjectAndContent obj512KiB = objectsFixture.getObj512KiB();
@@ -469,6 +506,29 @@ public void responseWith416ReturnsZeroAndLeavesTheChannelOpen() throws IOExcepti
469506
}
470507
}
471508

509+
/** Read channel does not consider itself closed once it returns {@code -1} from read. */
510+
@Test
511+
public void readChannelIsAlwaysOpen_willReturnNegative1UntilExplicitlyClosed() throws Exception {
512+
int length = 10;
513+
byte[] bytes = DataGenerator.base64Characters().genBytes(length);
514+
515+
BlobInfo info1 = BlobInfo.newBuilder(bucket, generator.randomObjectName()).build();
516+
Blob gen1 = storage.create(info1, bytes, BlobTargetOption.doesNotExist());
517+
518+
try (ReadChannel reader = storage.reader(gen1.getBlobId())) {
519+
ByteBuffer buf = ByteBuffer.allocate(length * 2);
520+
int read = reader.read(buf);
521+
assertAll(
522+
() -> assertThat(read).isEqualTo(length), () -> assertThat(reader.isOpen()).isTrue());
523+
int read2 = reader.read(buf);
524+
assertAll(() -> assertThat(read2).isEqualTo(-1), () -> assertThat(reader.isOpen()).isTrue());
525+
int read3 = reader.read(buf);
526+
assertAll(() -> assertThat(read3).isEqualTo(-1), () -> assertThat(reader.isOpen()).isTrue());
527+
reader.close();
528+
assertThrows(ClosedChannelException.class, () -> reader.read(buf));
529+
}
530+
}
531+
472532
private void captureAndRestoreTest(@Nullable Integer position, @Nullable Integer endOffset)
473533
throws IOException {
474534
ObjectAndContent obj512KiB = objectsFixture.getObj512KiB();

0 commit comments

Comments
 (0)