Skip to content

Commit 6eef1b0

Browse files
authored
feat: add per-message checksum validation for gRPC ReadObject operations (googleapis#3336)
1 parent 2698d43 commit 6eef1b0

File tree

4 files changed

+73
-57
lines changed

4 files changed

+73
-57
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ private ReadableByteChannelSessionBuilder(
6767
this.read = read;
6868
this.retrier = retrier;
6969
this.resultRetryAlgorithm = resultRetryAlgorithm;
70-
this.hasher = Hasher.noop();
70+
this.hasher = Hasher.defaultHasher();
7171
this.autoGzipDecompression = false;
7272
}
7373

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

Lines changed: 63 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import com.google.cloud.storage.Conversions.Decoder;
3131
import com.google.cloud.storage.Crc32cValue.Crc32cLengthKnown;
3232
import com.google.cloud.storage.GrpcUtils.ZeroCopyServerStreamingCallable;
33+
import com.google.cloud.storage.Hasher.UncheckedChecksumMismatchException;
3334
import com.google.cloud.storage.ResponseContentLifecycleHandle.ChildRef;
3435
import com.google.cloud.storage.Retrying.Retrier;
3536
import com.google.cloud.storage.UnbufferedReadableByteChannelSession.UnbufferedReadableByteChannel;
@@ -104,7 +105,11 @@ public boolean shouldRetry(
104105
boolean isWatchdogTimeout =
105106
previousThrowable instanceof StorageException
106107
&& previousThrowable.getCause() instanceof WatchdogTimeoutException;
107-
boolean shouldRetry = isWatchdogTimeout || alg.shouldRetry(previousThrowable, null);
108+
boolean isChecksumMismatch =
109+
previousThrowable instanceof StorageException
110+
&& previousThrowable.getCause() instanceof UncheckedChecksumMismatchException;
111+
boolean shouldRetry =
112+
isWatchdogTimeout || isChecksumMismatch || alg.shouldRetry(previousThrowable, null);
108113
if (previousThrowable != null && !shouldRetry) {
109114
result.setException(previousThrowable);
110115
}
@@ -146,59 +151,38 @@ public long read(ByteBuffer[] dsts, int offset, int length) throws IOException {
146151
Thread.currentThread().interrupt();
147152
throw new InterruptedIOException();
148153
}
154+
if (take instanceof IOException) {
155+
IOException ioe = (IOException) take;
156+
if (alg.shouldRetry(ioe, null)) {
157+
readObjectObserver = null;
158+
continue;
159+
} else {
160+
ioe.addSuppressed(new AsyncStorageTaskException());
161+
throw ioe;
162+
}
163+
}
149164
if (take instanceof Throwable) {
150165
Throwable throwable = (Throwable) take;
151166
BaseServiceException coalesce = StorageException.coalesce(throwable);
152167
if (alg.shouldRetry(coalesce, null)) {
153168
readObjectObserver = null;
154169
continue;
155170
} else {
171+
close();
156172
throw new IOException(coalesce);
157173
}
158174
}
159175
if (take == EOF_MARKER) {
160176
complete = true;
161177
break;
162178
}
163-
readObjectObserver.request();
164179

165-
ReadObjectResponse resp = (ReadObjectResponse) take;
166-
try (ResponseContentLifecycleHandle<ReadObjectResponse> handle =
167-
read.getResponseContentLifecycleManager().get(resp)) {
168-
ReadObjectResponseChildRef ref = ReadObjectResponseChildRef.from(handle);
169-
if (resp.hasMetadata()) {
170-
Object respMetadata = resp.getMetadata();
171-
if (metadata == null) {
172-
metadata = respMetadata;
173-
} else if (metadata.getGeneration() != respMetadata.getGeneration()) {
174-
throw closeWithError(
175-
String.format(
176-
Locale.US,
177-
"Mismatch Generation between subsequent reads. Expected %d but received %d",
178-
metadata.getGeneration(),
179-
respMetadata.getGeneration()));
180-
}
181-
}
182-
ChecksummedData checksummedData = resp.getChecksummedData();
183-
ByteString content = checksummedData.getContent();
184-
int contentSize = content.size();
185-
// Very important to know whether a crc32c value is set. Without checking, protobuf will
186-
// happily return 0, which is a valid crc32c value.
187-
if (checksummedData.hasCrc32C()) {
188-
Crc32cLengthKnown expected = Crc32cValue.of(checksummedData.getCrc32C(), contentSize);
189-
try {
190-
hasher.validate(expected, content);
191-
} catch (IOException e) {
192-
close();
193-
throw e;
194-
}
195-
}
196-
ref.copy(c, dsts, offset, length);
197-
if (ref.hasRemaining()) {
198-
leftovers = ref;
199-
} else {
200-
ref.close();
201-
}
180+
ReadObjectResponseChildRef ref = (ReadObjectResponseChildRef) take;
181+
ref.copy(c, dsts, offset, length);
182+
if (ref.hasRemaining()) {
183+
leftovers = ref;
184+
} else {
185+
ref.close();
202186
}
203187
}
204188
long read = c.read();
@@ -321,11 +305,10 @@ private void ensureStreamOpen() {
321305
}
322306
}
323307

324-
private IOException closeWithError(String message) throws IOException {
325-
close();
308+
private IOException createError(String message) throws IOException {
326309
StorageException cause =
327310
new StorageException(HttpStatusCodes.STATUS_CODE_PRECONDITION_FAILED, message);
328-
throw new IOException(message, cause);
311+
return new IOException(message, cause);
329312
}
330313

331314
private final class ReadObjectObserver extends StateCheckingResponseObserver<ReadObjectResponse> {
@@ -335,10 +318,6 @@ private final class ReadObjectObserver extends StateCheckingResponseObserver<Rea
335318

336319
private volatile StreamController controller;
337320

338-
void request() {
339-
controller.request(1);
340-
}
341-
342321
void cancel() {
343322
controller.cancel();
344323
}
@@ -352,16 +331,50 @@ protected void onStartImpl(StreamController controller) {
352331

353332
@Override
354333
protected void onResponseImpl(ReadObjectResponse response) {
355-
try {
356-
open.set(null);
357-
queue.offer(response);
358-
fetchOffset.addAndGet(response.getChecksummedData().getContent().size());
334+
controller.request(1);
335+
open.set(null);
336+
try (ResponseContentLifecycleHandle<ReadObjectResponse> handle =
337+
read.getResponseContentLifecycleManager().get(response)) {
338+
ChecksummedData checksummedData = response.getChecksummedData();
339+
ByteString content = checksummedData.getContent();
340+
int contentSize = content.size();
341+
// Very important to know whether a crc32c value is set. Without checking, protobuf will
342+
// happily return 0, which is a valid crc32c value.
343+
if (checksummedData.hasCrc32C()) {
344+
Crc32cLengthKnown expected = Crc32cValue.of(checksummedData.getCrc32C(), contentSize);
345+
try {
346+
hasher.validateUnchecked(expected, content);
347+
} catch (UncheckedChecksumMismatchException e) {
348+
queue.offer(e);
349+
return;
350+
}
351+
}
352+
if (response.hasMetadata()) {
353+
Object respMetadata = response.getMetadata();
354+
if (metadata == null) {
355+
metadata = respMetadata;
356+
} else if (metadata.getGeneration() != respMetadata.getGeneration()) {
357+
IOException exception =
358+
createError(
359+
String.format(
360+
Locale.US,
361+
"Mismatch Generation between subsequent reads. Expected %d but received %d",
362+
metadata.getGeneration(),
363+
respMetadata.getGeneration()));
364+
queue.offer(exception);
365+
return;
366+
}
367+
}
368+
queue.offer(ReadObjectResponseChildRef.from(handle));
369+
fetchOffset.addAndGet(contentSize);
359370
if (response.hasMetadata() && !result.isDone()) {
360371
result.set(response.getMetadata());
361372
}
362373
} catch (InterruptedException e) {
363374
Thread.currentThread().interrupt();
364375
throw Code.ABORTED.toStatus().withCause(e).asRuntimeException();
376+
} catch (IOException e) {
377+
throw new RuntimeException(e);
365378
}
366379
}
367380

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ protected LazyReadChannel<?, Object> newLazyReadChannel() {
6262
ResumableMedia.gapic()
6363
.read()
6464
.byteChannel(read, retrier, resultRetryAlgorithm)
65-
.setHasher(Hasher.noop())
65+
.setHasher(Hasher.defaultHasher())
6666
.setAutoGzipDecompression(autoGzipDecompression);
6767
BufferHandle bufferHandle = getBufferHandle();
6868
// because we're erasing the specific type of channel, we need to declare it here.

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

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import static com.google.cloud.storage.TestUtils.apiException;
2020
import static com.google.cloud.storage.TestUtils.getChecksummedData;
21+
import static com.google.cloud.storage.TestUtils.xxd;
2122
import static com.google.common.truth.Truth.assertThat;
2223
import static org.junit.Assert.assertThrows;
2324

@@ -236,7 +237,7 @@ public void readObject(
236237
}
237238

238239
@Test
239-
public void ioException_if_crc32c_mismatch_individual_message()
240+
public void ifCrc32cMismatchIndividualMessage_restartFromCorrectOffset()
240241
throws IOException, InterruptedException {
241242
StorageGrpc.StorageImplBase fakeStorage =
242243
new StorageGrpc.StorageImplBase() {
@@ -245,10 +246,12 @@ public void readObject(
245246
ReadObjectRequest request, StreamObserver<ReadObjectResponse> responseObserver) {
246247
if (request.equals(req1)) {
247248
responseObserver.onNext(resp1);
248-
ReadObjectResponse.Builder b = resp2.toBuilder();
249+
responseObserver.onNext(resp2);
250+
ReadObjectResponse.Builder b = resp3.toBuilder();
249251
// set a bad checksum value
250252
b.getChecksummedDataBuilder().setCrc32C(1);
251253
responseObserver.onNext(b.build());
254+
} else if (request.equals(req2)) {
252255
responseObserver.onNext(resp3);
253256
responseObserver.onNext(resp4);
254257
responseObserver.onCompleted();
@@ -276,10 +279,10 @@ public void readObject(
276279
retryOnly(DataLossException.class)));
277280
byte[] actualBytes = new byte[40];
278281
try (UnbufferedReadableByteChannel c = session.open()) {
279-
IOException ioException =
280-
assertThrows(IOException.class, () -> c.read(ByteBuffer.wrap(actualBytes)));
282+
int read = c.read(ByteBuffer.wrap(actualBytes));
281283

282-
assertThat(ioException).hasMessageThat().contains("Mismatch checksum");
284+
assertThat(read).isEqualTo(40);
285+
assertThat(xxd(actualBytes)).isEqualTo(xxd(bytes));
283286
}
284287
}
285288
}

0 commit comments

Comments
 (0)