Skip to content

Commit ecaaceb

Browse files
nidhiii-27Dhriti07
authored andcommitted
feat(storage): log additional bytes received from GCS in read path (#13427)
Fixes bug 475824752. Tracks the number of bytes received from the transport layer and emits a warning log if GCS sends more bytes than requested by the user during explicit range requests. [Generated-by: AI] --------- Co-authored-by: Dhriti07 <56169283+Dhriti07@users.noreply.github.com>
1 parent 243f29b commit ecaaceb

4 files changed

Lines changed: 196 additions & 30 deletions

File tree

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

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ class ApiaryUnbufferedReadableByteChannel implements UnbufferedReadableByteChann
7777
private ScatteringByteChannel sbc;
7878
private boolean open;
7979
private boolean returnEOF;
80+
private long totalBytesReadFromNetwork;
8081

8182
// returned X-Goog-Generation header value
8283
private Long xGoogGeneration;
@@ -147,6 +148,7 @@ public long read(ByteBuffer[] dsts, int offset, int length) throws IOException {
147148
}
148149
} else {
149150
totalRead += read;
151+
totalBytesReadFromNetwork += read;
150152
}
151153
return totalRead;
152154
} catch (Exception t) {
@@ -182,9 +184,25 @@ public boolean isOpen() {
182184

183185
@Override
184186
public void close() throws IOException {
185-
open = false;
186-
if (sbc != null) {
187-
sbc.close();
187+
try {
188+
long requestedLength = apiaryReadRequest.getByteRangeSpec().length();
189+
if (requestedLength >= 0
190+
&& requestedLength < ByteRangeSpec.EFFECTIVE_INFINITY
191+
&& totalBytesReadFromNetwork > requestedLength) {
192+
java.util.logging.Logger.getLogger(ApiaryUnbufferedReadableByteChannel.class.getName())
193+
.warning(
194+
String.format(
195+
"storage: received %d more bytes than requested from GCS for bucket '%s',"
196+
+ " object '%s'",
197+
totalBytesReadFromNetwork - requestedLength,
198+
apiaryReadRequest.getObject().getBucket(),
199+
apiaryReadRequest.getObject().getName()));
200+
}
201+
} finally {
202+
open = false;
203+
if (sbc != null) {
204+
sbc.close();
205+
}
188206
}
189207
}
190208

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

Lines changed: 39 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -209,37 +209,50 @@ public boolean isOpen() {
209209

210210
@Override
211211
public void close() throws IOException {
212-
open = false;
213212
try {
214-
if (leftovers != null) {
215-
leftovers.close();
213+
long readLimit = req.getReadLimit();
214+
long receivedBytes = fetchOffset.get() - req.getReadOffset();
215+
if (readLimit > 0 && receivedBytes > readLimit) {
216+
java.util.logging.Logger.getLogger(GapicUnbufferedReadableByteChannel.class.getName())
217+
.warning(
218+
String.format(
219+
"storage: received %d more bytes than requested from GCS for bucket '%s',"
220+
+ " object '%s'",
221+
receivedBytes - readLimit, req.getBucket(), req.getObject()));
216222
}
217-
ReadObjectObserver obs = readObjectObserver;
218-
if (obs != null && !obs.cancellation.isDone()) {
219-
obs.cancel();
220-
drainQueue();
221-
try {
222-
// make sure our waiting doesn't lockup permanently
223-
obs.cancellation.get(1, TimeUnit.SECONDS);
224-
} catch (InterruptedException e) {
225-
Thread.currentThread().interrupt();
226-
InterruptedIOException ioe = new InterruptedIOException();
227-
ioe.initCause(e);
228-
ioe.addSuppressed(new AsyncStorageTaskException());
229-
throw ioe;
230-
} catch (ExecutionException e) {
231-
Throwable cause = e;
232-
if (e.getCause() != null) {
233-
cause = e.getCause();
223+
} finally {
224+
open = false;
225+
try {
226+
if (leftovers != null) {
227+
leftovers.close();
228+
}
229+
ReadObjectObserver obs = readObjectObserver;
230+
if (obs != null && !obs.cancellation.isDone()) {
231+
obs.cancel();
232+
drainQueue();
233+
try {
234+
// make sure our waiting doesn't lockup permanently
235+
obs.cancellation.get(1, TimeUnit.SECONDS);
236+
} catch (InterruptedException e) {
237+
Thread.currentThread().interrupt();
238+
InterruptedIOException ioe = new InterruptedIOException();
239+
ioe.initCause(e);
240+
ioe.addSuppressed(new AsyncStorageTaskException());
241+
throw ioe;
242+
} catch (ExecutionException e) {
243+
Throwable cause = e;
244+
if (e.getCause() != null) {
245+
cause = e.getCause();
246+
}
247+
IOException ioException = new IOException(cause);
248+
ioException.addSuppressed(new AsyncStorageTaskException());
249+
throw ioException;
250+
} catch (TimeoutException ignore) {
234251
}
235-
IOException ioException = new IOException(cause);
236-
ioException.addSuppressed(new AsyncStorageTaskException());
237-
throw ioException;
238-
} catch (TimeoutException ignore) {
239252
}
253+
} finally {
254+
drainQueue();
240255
}
241-
} finally {
242-
drainQueue();
243256
}
244257
}
245258

java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/ApiaryUnbufferedReadableByteChannelTest.java

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package com.google.cloud.storage;
1818

19+
import static com.google.common.truth.Truth.assertThat;
1920
import static org.junit.Assert.assertArrayEquals;
2021
import static org.junit.Assert.assertThrows;
2122
import static org.junit.Assert.assertTrue;
@@ -27,17 +28,25 @@
2728
import com.google.api.services.storage.Storage;
2829
import com.google.api.services.storage.model.StorageObject;
2930
import com.google.cloud.storage.ApiaryUnbufferedReadableByteChannel.ApiaryReadRequest;
31+
import com.google.cloud.storage.Retrying.Retrier;
3032
import com.google.cloud.storage.Retrying.RetrierWithAlg;
33+
import com.google.cloud.storage.UnbufferedReadableByteChannelSession.UnbufferedReadableByteChannel;
34+
import com.google.cloud.storage.spi.v1.AuditingHttpTransport;
3135
import com.google.common.collect.ImmutableMap;
3236
import java.io.ByteArrayOutputStream;
3337
import java.io.IOException;
3438
import java.nio.ByteBuffer;
3539
import java.nio.channels.Channels;
3640
import java.nio.channels.WritableByteChannel;
41+
import java.util.ArrayList;
42+
import java.util.List;
3743
import java.util.Map;
44+
import java.util.logging.Handler;
45+
import java.util.logging.LogRecord;
46+
import java.util.logging.Logger;
3847
import org.junit.Test;
3948

40-
public class ApiaryUnbufferedReadableByteChannelTest {
49+
public final class ApiaryUnbufferedReadableByteChannelTest {
4150

4251
private static final byte[] CONTENT_BYTES = "Hello, World!".getBytes();
4352
private static final String CORRECT_CRC32C_BASE64 = "TVUQaA==";
@@ -218,4 +227,61 @@ public void testRead_partialRangeNoCrc32cValidation() throws IOException {
218227
assertArrayEquals(CONTENT_BYTES, out.toByteArray());
219228
}
220229
}
230+
231+
@Test
232+
public void logsWarning_whenReceivingMoreBytesThanRequested() throws IOException {
233+
byte[] content = "0123456789extra_bytes".getBytes();
234+
MockLowLevelHttpResponse response =
235+
new MockLowLevelHttpResponse()
236+
.setContentType("application/octet-stream")
237+
.setContent(content)
238+
.setStatusCode(200);
239+
240+
AuditingHttpTransport transport = new AuditingHttpTransport(response);
241+
Storage storage =
242+
new Storage.Builder(transport, GsonFactory.getDefaultInstance(), null).build();
243+
244+
StorageObject storageObject = new StorageObject().setBucket("bucket").setName("object");
245+
// Explicit range request for 10 bytes
246+
ByteRangeSpec byteRangeSpec = ByteRangeSpec.relativeLength(0L, 10L);
247+
ApiaryReadRequest apiaryReadRequest =
248+
new ApiaryReadRequest(storageObject, ImmutableMap.of(), byteRangeSpec);
249+
250+
Logger logger = Logger.getLogger(ApiaryUnbufferedReadableByteChannel.class.getName());
251+
List<LogRecord> records = new ArrayList<>();
252+
Handler handler =
253+
new Handler() {
254+
@Override
255+
public void publish(LogRecord record) {
256+
records.add(record);
257+
}
258+
259+
@Override
260+
public void flush() {}
261+
262+
@Override
263+
public void close() throws SecurityException {}
264+
};
265+
logger.addHandler(handler);
266+
267+
try {
268+
try (UnbufferedReadableByteChannel c =
269+
new ApiaryUnbufferedReadableByteChannel(
270+
apiaryReadRequest,
271+
storage,
272+
SettableApiFuture.<StorageObject>create(),
273+
Retrier.attemptOnce(),
274+
Retrying.neverRetry(),
275+
Hasher.defaultHasher())) {
276+
ByteBuffer dst = ByteBuffer.allocate(25);
277+
c.read(dst);
278+
}
279+
280+
boolean warningLogged =
281+
records.stream().anyMatch(r -> r.getMessage().contains("more bytes than requested"));
282+
assertThat(warningLogged).isTrue();
283+
} finally {
284+
logger.removeHandler(handler);
285+
}
286+
}
221287
}

java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/ITGapicUnbufferedReadableByteChannelTest.java

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,75 @@ public void readObject(
330330
}
331331
}
332332

333+
@Test
334+
public void logsWarning_whenReceivingMoreBytesThanRequested()
335+
throws IOException, ExecutionException, InterruptedException, TimeoutException {
336+
ReadObjectRequest reqWithLimit =
337+
ReadObjectRequest.newBuilder()
338+
.setObject(objectName)
339+
.setReadOffset(0)
340+
.setReadLimit(10)
341+
.build();
342+
343+
StorageGrpc.StorageImplBase fakeStorage =
344+
new StorageGrpc.StorageImplBase() {
345+
@Override
346+
public void readObject(
347+
ReadObjectRequest request, StreamObserver<ReadObjectResponse> responseObserver) {
348+
responseObserver.onNext(resp1); // sends 10 bytes
349+
responseObserver.onNext(resp2); // sends another 10 bytes (total 20 > limit 10)
350+
responseObserver.onCompleted();
351+
}
352+
};
353+
354+
java.util.logging.Logger logger =
355+
java.util.logging.Logger.getLogger(GapicUnbufferedReadableByteChannel.class.getName());
356+
java.util.List<java.util.logging.LogRecord> records = new java.util.ArrayList<>();
357+
java.util.logging.Handler handler =
358+
new java.util.logging.Handler() {
359+
@Override
360+
public void publish(java.util.logging.LogRecord record) {
361+
records.add(record);
362+
}
363+
364+
@Override
365+
public void flush() {}
366+
367+
@Override
368+
public void close() throws SecurityException {}
369+
};
370+
logger.addHandler(handler);
371+
372+
try (FakeServer server = FakeServer.of(fakeStorage);
373+
StorageClient storageClient = StorageClient.create(server.storageSettings())) {
374+
Retrier retrier = TestUtils.retrierFromStorageOptions(server.getGrpcStorageOptions());
375+
376+
UnbufferedReadableByteChannelSession<Object> session =
377+
new UnbufferedReadSession<>(
378+
ApiFutures.immediateFuture(reqWithLimit),
379+
(start, resultFuture) ->
380+
new GapicUnbufferedReadableByteChannel(
381+
resultFuture,
382+
new ZeroCopyServerStreamingCallable<>(
383+
storageClient.readObjectCallable(),
384+
ResponseContentLifecycleManager.noop()),
385+
start,
386+
Hasher.noop(),
387+
retrier,
388+
retryOnly(DataLossException.class)));
389+
byte[] actualBytes = new byte[15];
390+
try (UnbufferedReadableByteChannel c = session.open()) {
391+
c.read(ByteBuffer.wrap(actualBytes));
392+
}
393+
394+
boolean warningLogged =
395+
records.stream().anyMatch(r -> r.getMessage().contains("more bytes than requested"));
396+
assertThat(warningLogged).isTrue();
397+
} finally {
398+
logger.removeHandler(handler);
399+
}
400+
}
401+
333402
private static <E extends ApiException> ResultRetryAlgorithm<?> retryOnly(Class<E> c) {
334403
return new BasicResultRetryAlgorithm<java.lang.Object>() {
335404
@Override

0 commit comments

Comments
 (0)