Skip to content

Commit c26091e

Browse files
feat: add support to a few more specific StorageErrors for the Write API (#1563)
* feat: add support to a few more specific StorageErrors for the Write API OFFSET_OUT_OF_RANGE OFFSET_ALREADY_EXISTS STREAM_NOT_FOUND Towards b/220198094 * add integration test for streamNotFound * add integration test for streamNotFound * add more changes since backend changes have rolled out * update clirr-ignored-differences.xml * update IT * nit * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * add error message match pattern * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md Co-authored-by: Owl Bot <gcf-owl-bot[bot]@users.noreply.github.com>
1 parent 60ed6b0 commit c26091e

File tree

3 files changed

+158
-16
lines changed

3 files changed

+158
-16
lines changed

google-cloud-bigquerystorage/clirr-ignored-differences.xml

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,13 @@
22
<!-- see http://www.mojohaus.org/clirr-maven-plugin/examples/ignored-differences.html -->
33
<differences>
44
<difference>
5-
<differenceType>7005</differenceType>
5+
<differenceType>7004</differenceType>
66
<className>com/google/cloud/bigquery/storage/v1/Exceptions$SchemaMismatchedException</className>
7-
<method>Exceptions$SchemaMismatchedException(java.lang.String, java.lang.String, java.lang.Throwable)</method>
8-
<to>Exceptions$SchemaMismatchedException(io.grpc.Status, io.grpc.Metadata, java.lang.String)</to>
7+
<method>Exceptions$SchemaMismatchedException(io.grpc.Status, io.grpc.Metadata, java.lang.String)</method>
98
</difference>
109
<difference>
11-
<differenceType>7005</differenceType>
10+
<differenceType>7004</differenceType>
1211
<className>com/google/cloud/bigquery/storage/v1/Exceptions$StreamFinalizedException</className>
13-
<method>Exceptions$StreamFinalizedException(java.lang.String, java.lang.String, java.lang.Throwable)</method>
14-
<to>Exceptions$StreamFinalizedException(io.grpc.Status, io.grpc.Metadata, java.lang.String)</to>
12+
<method>Exceptions$StreamFinalizedException(io.grpc.Status, io.grpc.Metadata, java.lang.String)</method>
1513
</difference>
1614
</differences>

google-cloud-bigquerystorage/src/main/java/com/google/cloud/bigquery/storage/v1/Exceptions.java

Lines changed: 83 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,11 @@
1919
import com.google.common.collect.ImmutableMap;
2020
import com.google.protobuf.Any;
2121
import com.google.protobuf.InvalidProtocolBufferException;
22-
import io.grpc.Metadata;
2322
import io.grpc.Status;
2423
import io.grpc.StatusRuntimeException;
2524
import io.grpc.protobuf.StatusProto;
25+
import java.util.regex.Matcher;
26+
import java.util.regex.Pattern;
2627
import javax.annotation.Nullable;
2728

2829
/** Exceptions for Storage Client Libraries. */
@@ -37,18 +38,23 @@ public static class StorageException extends StatusRuntimeException {
3738

3839
private final ImmutableMap<String, GrpcStatusCode> errors;
3940
private final String streamName;
41+
private final Long expectedOffset;
42+
private final Long actualOffset;
4043

4144
private StorageException() {
42-
this(null, null, null, ImmutableMap.of());
45+
this(null, null, null, null, ImmutableMap.of());
4346
}
4447

4548
private StorageException(
4649
@Nullable Status grpcStatus,
47-
@Nullable Metadata metadata,
4850
@Nullable String streamName,
51+
@Nullable Long expectedOffset,
52+
@Nullable Long actualOffset,
4953
ImmutableMap<String, GrpcStatusCode> errors) {
50-
super(grpcStatus, metadata);
54+
super(grpcStatus);
5155
this.streamName = streamName;
56+
this.expectedOffset = expectedOffset;
57+
this.actualOffset = actualOffset;
5258
this.errors = errors;
5359
}
5460

@@ -59,12 +65,20 @@ public ImmutableMap<String, GrpcStatusCode> getErrors() {
5965
public String getStreamName() {
6066
return streamName;
6167
}
68+
69+
public long getExpectedOffset() {
70+
return expectedOffset;
71+
}
72+
73+
public long getActualOffset() {
74+
return actualOffset;
75+
}
6276
}
6377

6478
/** Stream has already been finalized. */
6579
public static final class StreamFinalizedException extends StorageException {
66-
protected StreamFinalizedException(Status grpcStatus, Metadata metadata, String name) {
67-
super(grpcStatus, metadata, name, ImmutableMap.of());
80+
protected StreamFinalizedException(Status grpcStatus, String name) {
81+
super(grpcStatus, name, null, null, ImmutableMap.of());
6882
}
6983
}
7084

@@ -73,8 +87,31 @@ protected StreamFinalizedException(Status grpcStatus, Metadata metadata, String
7387
* This can be resolved by updating the table's schema with the message schema.
7488
*/
7589
public static final class SchemaMismatchedException extends StorageException {
76-
protected SchemaMismatchedException(Status grpcStatus, Metadata metadata, String name) {
77-
super(grpcStatus, metadata, name, ImmutableMap.of());
90+
protected SchemaMismatchedException(Status grpcStatus, String name) {
91+
super(grpcStatus, name, null, null, ImmutableMap.of());
92+
}
93+
}
94+
95+
/** Offset already exists. */
96+
public static final class OffsetAlreadyExists extends StorageException {
97+
protected OffsetAlreadyExists(
98+
Status grpcStatus, String name, Long expectedOffset, Long actualOffset) {
99+
super(grpcStatus, name, expectedOffset, actualOffset, ImmutableMap.of());
100+
}
101+
}
102+
103+
/** Offset out of range. */
104+
public static final class OffsetOutOfRange extends StorageException {
105+
protected OffsetOutOfRange(
106+
Status grpcStatus, String name, Long expectedOffset, Long actualOffset) {
107+
super(grpcStatus, name, expectedOffset, actualOffset, ImmutableMap.of());
108+
}
109+
}
110+
111+
/** Stream is not found. */
112+
public static final class StreamNotFound extends StorageException {
113+
protected StreamNotFound(Status grpcStatus, String name) {
114+
super(grpcStatus, name, null, null, ImmutableMap.of());
78115
}
79116
}
80117

@@ -106,12 +143,48 @@ public static StorageException toStorageException(
106143
if (error == null) {
107144
return null;
108145
}
146+
String streamName = error.getEntity();
147+
// The error message should have Entity but it's missing from the message for
148+
// OFFSET_ALREADY_EXISTS
149+
// TODO: Simplify the logic below when backend fixes passing Entity for OFFSET_ALREADY_EXISTS
150+
// error
151+
String errorMessage =
152+
error.getErrorMessage().indexOf("Entity") > 0
153+
? error.getErrorMessage().substring(0, error.getErrorMessage().indexOf("Entity")).trim()
154+
: error.getErrorMessage().trim();
155+
156+
// Ensure that erro message has the desirable pattern for parsing
157+
String errormessagePatternString = "expected offset [0-9]+, received [0-9]+";
158+
Pattern errorMessagePattern = Pattern.compile(errormessagePatternString);
159+
Matcher errorMessageMatcher = errorMessagePattern.matcher(errorMessage);
160+
161+
Long expectedOffet;
162+
Long actualOffset;
163+
if (!errorMessageMatcher.find()) {
164+
expectedOffet = -1L;
165+
actualOffset = -1L;
166+
} else {
167+
expectedOffet =
168+
Long.parseLong(
169+
errorMessage.substring(
170+
errorMessage.lastIndexOf("offset") + 7, errorMessage.lastIndexOf(",")));
171+
actualOffset = Long.parseLong(errorMessage.substring(errorMessage.lastIndexOf(" ") + 1));
172+
}
109173
switch (error.getCode()) {
110174
case STREAM_FINALIZED:
111-
return new StreamFinalizedException(grpcStatus, null, error.getEntity());
175+
return new StreamFinalizedException(grpcStatus, streamName);
176+
177+
case STREAM_NOT_FOUND:
178+
return new StreamNotFound(grpcStatus, streamName);
112179

113180
case SCHEMA_MISMATCH_EXTRA_FIELDS:
114-
return new SchemaMismatchedException(grpcStatus, null, error.getEntity());
181+
return new SchemaMismatchedException(grpcStatus, streamName);
182+
183+
case OFFSET_OUT_OF_RANGE:
184+
return new OffsetOutOfRange(grpcStatus, streamName, expectedOffet, actualOffset);
185+
186+
case OFFSET_ALREADY_EXISTS:
187+
return new OffsetAlreadyExists(grpcStatus, streamName, expectedOffet, actualOffset);
115188

116189
default:
117190
return null;

google-cloud-bigquerystorage/src/test/java/com/google/cloud/bigquery/storage/v1/it/ITBigQueryWriteManualClientTest.java

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
import com.google.cloud.bigquery.Schema;
2929
import com.google.cloud.bigquery.storage.test.Test.*;
3030
import com.google.cloud.bigquery.storage.v1.*;
31+
import com.google.cloud.bigquery.storage.v1.Exceptions.OffsetAlreadyExists;
32+
import com.google.cloud.bigquery.storage.v1.Exceptions.OffsetOutOfRange;
3133
import com.google.cloud.bigquery.storage.v1.Exceptions.SchemaMismatchedException;
3234
import com.google.cloud.bigquery.storage.v1.Exceptions.StreamFinalizedException;
3335
import com.google.cloud.bigquery.testing.RemoteBigQueryHelper;
@@ -901,6 +903,75 @@ public void testStreamFinalizedError()
901903
}
902904
}
903905

906+
@Test
907+
public void testOffsetAlreadyExistsError()
908+
throws IOException, ExecutionException, InterruptedException {
909+
WriteStream writeStream =
910+
client.createWriteStream(
911+
CreateWriteStreamRequest.newBuilder()
912+
.setParent(tableId)
913+
.setWriteStream(
914+
WriteStream.newBuilder().setType(WriteStream.Type.COMMITTED).build())
915+
.build());
916+
try (StreamWriter streamWriter =
917+
StreamWriter.newBuilder(writeStream.getName())
918+
.setWriterSchema(ProtoSchemaConverter.convert(FooType.getDescriptor()))
919+
.build()) {
920+
// Append once with correct offset
921+
ApiFuture<AppendRowsResponse> response =
922+
streamWriter.append(CreateProtoRowsMultipleColumns(new String[] {"a"}), /*offset=*/ 0);
923+
response.get();
924+
// Append again with the same offset
925+
ApiFuture<AppendRowsResponse> response2 =
926+
streamWriter.append(CreateProtoRowsMultipleColumns(new String[] {"a"}), /*offset=*/ 0);
927+
try {
928+
response2.get();
929+
Assert.fail("Should fail");
930+
} catch (ExecutionException e) {
931+
assertEquals(Exceptions.OffsetAlreadyExists.class, e.getCause().getClass());
932+
Exceptions.OffsetAlreadyExists actualError = (OffsetAlreadyExists) e.getCause();
933+
assertNotNull(actualError.getStreamName());
934+
assertEquals(1, actualError.getExpectedOffset());
935+
assertEquals(0, actualError.getActualOffset());
936+
assertEquals(Code.ALREADY_EXISTS, Status.fromThrowable(e.getCause()).getCode());
937+
assertThat(e.getCause().getMessage())
938+
.contains("The offset is within stream, expected offset 1, received 0");
939+
}
940+
}
941+
}
942+
943+
@Test
944+
public void testOffsetOutOfRangeError() throws IOException, InterruptedException {
945+
WriteStream writeStream =
946+
client.createWriteStream(
947+
CreateWriteStreamRequest.newBuilder()
948+
.setParent(tableId)
949+
.setWriteStream(
950+
WriteStream.newBuilder().setType(WriteStream.Type.COMMITTED).build())
951+
.build());
952+
try (StreamWriter streamWriter =
953+
StreamWriter.newBuilder(writeStream.getName())
954+
.setWriterSchema(ProtoSchemaConverter.convert(FooType.getDescriptor()))
955+
.build()) {
956+
// Append with an out of range offset
957+
ApiFuture<AppendRowsResponse> response =
958+
streamWriter.append(CreateProtoRowsMultipleColumns(new String[] {"a"}), /*offset=*/ 10);
959+
try {
960+
response.get();
961+
Assert.fail("Should fail");
962+
} catch (ExecutionException e) {
963+
assertEquals(Exceptions.OffsetOutOfRange.class, e.getCause().getClass());
964+
Exceptions.OffsetOutOfRange actualError = (OffsetOutOfRange) e.getCause();
965+
assertNotNull(actualError.getStreamName());
966+
assertEquals(0, actualError.getExpectedOffset());
967+
assertEquals(10, actualError.getActualOffset());
968+
assertEquals(Code.OUT_OF_RANGE, Status.fromThrowable(e.getCause()).getCode());
969+
assertThat(e.getCause().getMessage())
970+
.contains("The offset is beyond stream, expected offset 0, received 10");
971+
}
972+
}
973+
}
974+
904975
@Test
905976
public void testStreamReconnect() throws IOException, InterruptedException, ExecutionException {
906977
WriteStream writeStream =

0 commit comments

Comments
 (0)