All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.cloud.bigquery.storage.v1.Exceptions Maven / Gradle / Ivy

/*
 * Copyright 2021 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.google.cloud.bigquery.storage.v1;

import com.google.api.gax.grpc.GrpcStatusCode;
import com.google.common.collect.ImmutableMap;
import com.google.protobuf.Any;
import com.google.protobuf.InvalidProtocolBufferException;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import io.grpc.protobuf.StatusProto;
import java.time.Duration;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nullable;

/** Exceptions for Storage Client Libraries. */
public final class Exceptions {
  /** Main Storage Exception. Might contain map of streams to errors for that stream. */
  public static class StorageException extends StatusRuntimeException {

    private final ImmutableMap errors;
    private final String streamName;
    private final Long expectedOffset;
    private final Long actualOffset;

    private StorageException() {
      this(null, null, null, null, ImmutableMap.of());
    }

    private StorageException(
        @Nullable Status grpcStatus,
        @Nullable String streamName,
        @Nullable Long expectedOffset,
        @Nullable Long actualOffset,
        ImmutableMap errors) {
      super(grpcStatus);
      this.streamName = streamName;
      this.expectedOffset = expectedOffset;
      this.actualOffset = actualOffset;
      this.errors = errors;
    }

    public ImmutableMap getErrors() {
      return errors;
    }

    public String getStreamName() {
      return streamName;
    }

    public long getExpectedOffset() {
      return expectedOffset;
    }

    public long getActualOffset() {
      return actualOffset;
    }
  }

  /**
   * The write stream has already been finalized and will not accept further appends or flushes. To
   * send additional requests, you will need to create a new write stream via CreateWriteStream.
   */
  public static final class StreamFinalizedException extends StorageException {
    protected StreamFinalizedException(Status grpcStatus, String name) {
      super(grpcStatus, name, null, null, ImmutableMap.of());
    }
  }

  /**
   * There was a schema mismatch due to bigquery table with fewer fields than the input message.
   * This can be resolved by updating the table's schema with the message schema.
   */
  public static final class SchemaMismatchedException extends StorageException {
    protected SchemaMismatchedException(Status grpcStatus, String name) {
      super(grpcStatus, name, null, null, ImmutableMap.of());
    }
  }

  /**
   * Offset already exists. This indicates that the append request attempted to write data to an
   * offset before the current end of the stream. This is an expected exception when ExactOnce is
   * enforced. You can safely ignore it, and keep appending until there is new data to append.
   */
  public static final class OffsetAlreadyExists extends StorageException {
    protected OffsetAlreadyExists(
        Status grpcStatus, String name, Long expectedOffset, Long actualOffset) {
      super(grpcStatus, name, expectedOffset, actualOffset, ImmutableMap.of());
    }
  }

  /**
   * Offset out of range. This indicates that the append request is attempting to write data to a
   * point beyond the current end of the stream. To append data successfully, you must either
   * specify the offset corresponding to the current end of stream, or omit the offset from the
   * append request. It usually means a bug in your code that introduces a gap in appends.
   */
  public static final class OffsetOutOfRange extends StorageException {
    protected OffsetOutOfRange(
        Status grpcStatus, String name, Long expectedOffset, Long actualOffset) {
      super(grpcStatus, name, expectedOffset, actualOffset, ImmutableMap.of());
    }
  }

  /**
   * The stream is not found. Possible causes include incorrectly specifying the stream identifier
   * or attempting to use an old stream identifier that no longer exists. You can invoke
   * CreateWriteStream to create a new stream.
   */
  public static final class StreamNotFound extends StorageException {
    protected StreamNotFound(Status grpcStatus, String name) {
      super(grpcStatus, name, null, null, ImmutableMap.of());
    }
  }

  private static StorageError toStorageError(com.google.rpc.Status rpcStatus) {
    for (Any detail : rpcStatus.getDetailsList()) {
      if (detail.is(StorageError.class)) {
        try {
          return detail.unpack(StorageError.class);
        } catch (InvalidProtocolBufferException protoException) {
          throw new IllegalStateException(protoException);
        }
      }
    }
    return null;
  }

  /**
   * Converts a c.g.rpc.Status into a StorageException, if possible. Examines the embedded
   * StorageError, and potentially returns a {@link StreamFinalizedException} or {@link
   * SchemaMismatchedException} (both derive from StorageException). If there is no StorageError, or
   * the StorageError is a different error it will return NULL.
   */
  @Nullable
  public static StorageException toStorageException(
      com.google.rpc.Status rpcStatus, Throwable exception) {
    if (rpcStatus == null) {
      return null;
    }
    StorageError error = toStorageError(rpcStatus);
    Status grpcStatus =
        Status.fromCodeValue(rpcStatus.getCode()).withDescription(rpcStatus.getMessage());
    if (error == null) {
      return null;
    }
    String streamName = error.getEntity();
    // The error message should have Entity but it's missing from the message for
    // OFFSET_ALREADY_EXISTS
    // TODO: Simplify the logic below when backend fixes passing Entity for OFFSET_ALREADY_EXISTS
    // error
    String errorMessage =
        error.getErrorMessage().indexOf("Entity") > 0
            ? error.getErrorMessage().substring(0, error.getErrorMessage().indexOf("Entity")).trim()
            : error.getErrorMessage().trim();

    // Ensure that erro message has the desirable pattern for parsing
    String errormessagePatternString = "expected offset [0-9]+, received [0-9]+";
    Pattern errorMessagePattern = Pattern.compile(errormessagePatternString);
    Matcher errorMessageMatcher = errorMessagePattern.matcher(errorMessage);

    Long expectedOffet;
    Long actualOffset;
    if (!errorMessageMatcher.find()) {
      expectedOffet = -1L;
      actualOffset = -1L;
    } else {
      expectedOffet =
          Long.parseLong(
              errorMessage.substring(
                  errorMessage.lastIndexOf("offset") + 7, errorMessage.lastIndexOf(",")));
      actualOffset = Long.parseLong(errorMessage.substring(errorMessage.lastIndexOf(" ") + 1));
    }
    switch (error.getCode()) {
      case STREAM_FINALIZED:
        return new StreamFinalizedException(grpcStatus, streamName);

      case STREAM_NOT_FOUND:
        return new StreamNotFound(grpcStatus, streamName);

      case SCHEMA_MISMATCH_EXTRA_FIELDS:
        return new SchemaMismatchedException(grpcStatus, streamName);

      case OFFSET_OUT_OF_RANGE:
        return new OffsetOutOfRange(grpcStatus, streamName, expectedOffet, actualOffset);

      case OFFSET_ALREADY_EXISTS:
        return new OffsetAlreadyExists(grpcStatus, streamName, expectedOffet, actualOffset);

      default:
        return null;
    }
  }

  /**
   * Converts a Throwable into a StorageException, if possible. Examines the embedded error message,
   * and potentially returns a {@link StreamFinalizedException} or {@link SchemaMismatchedException}
   * (both derive from StorageException). If there is no StorageError, or the StorageError is a
   * different error it will return NULL.
   */
  @Nullable
  public static StorageException toStorageException(Throwable exception) {
    com.google.rpc.Status rpcStatus = StatusProto.fromThrowable(exception);
    return toStorageException(rpcStatus, exception);
  }

  /**
   * This class has a typo in the name. It will be removed soon. Please use {@link
   * AppendSerializationError}
   */
  public static class AppendSerializtionError extends StatusRuntimeException {
    private final Map rowIndexToErrorMessage;
    private final String streamName;

    public AppendSerializtionError(
        int codeValue,
        String description,
        String streamName,
        Map rowIndexToErrorMessage) {
      super(Status.fromCodeValue(codeValue).withDescription(description));
      this.rowIndexToErrorMessage = rowIndexToErrorMessage;
      this.streamName = streamName;
    }

    public Map getRowIndexToErrorMessage() {
      return rowIndexToErrorMessage;
    }

    public String getStreamName() {
      return streamName;
    }
  }

  /**
   * This exception is thrown from {@link SchemaAwareStreamWriter#append(Iterable)} when the client
   * side Proto serialization fails. It can also be thrown by the server in case rows contains
   * invalid data. The exception contains a Map of indexes of faulty rows and the corresponding
   * error message.
   */
  public static class AppendSerializationError extends AppendSerializtionError {

    public AppendSerializationError(
        int codeValue,
        String description,
        String streamName,
        Map rowIndexToErrorMessage) {
      super(codeValue, description, streamName, rowIndexToErrorMessage);
    }
  }

  /** This exception is thrown from proto converter to wrap the row index to error mapping. */
  static class RowIndexToErrorException extends IllegalArgumentException {
    Map rowIndexToErrorMessage;

    boolean hasDataUnknownError;

    public RowIndexToErrorException(
        Map rowIndexToErrorMessage, boolean hasDataUnknownError) {
      this.rowIndexToErrorMessage = rowIndexToErrorMessage;
      this.hasDataUnknownError = hasDataUnknownError;
    }

    // This message should not be exposed to the user directly.
    // Please examine individual row's error through `rowIndexToErrorMessage`.
    public String getMessage() {
      return "The map of row index to error message is " + rowIndexToErrorMessage.toString();
    }

    public boolean hasDataUnknownError() {
      return hasDataUnknownError;
    }
  }

  /** This exception is used internally to handle field level parsing errors. */
  public static class FieldParseError extends IllegalArgumentException {
    private final String fieldName;
    private final String bqType;
    private final Throwable cause;

    protected FieldParseError(String fieldName, String bqType, Throwable cause) {
      this.fieldName = fieldName;
      this.bqType = bqType;
      this.cause = cause;
    }

    public String getFieldName() {
      return fieldName;
    }

    public String getBqType() {
      return bqType;
    }

    public Throwable getCause() {
      return cause;
    }

    public String getMessage() {
      return cause.getMessage();
    }
  }

  /**
   * This writer instance has either been closed by the user explicitly, or has encountered
   * non-retriable errors.
   *
   * 

To continue to write to the same stream, you will need to create a new writer instance. */ public static final class StreamWriterClosedException extends StatusRuntimeException { private final String streamName; private final String writerId; protected StreamWriterClosedException(Status grpcStatus, String streamName, String writerId) { super(grpcStatus); this.streamName = streamName; this.writerId = writerId; } public String getStreamName() { return streamName; } public String getWriterId() { return writerId; } } /** * If FlowController.LimitExceededBehavior is set to Block and inflight limit is exceeded, this * exception will be thrown. If it is just a spike, you may retry the request. Otherwise, you can * increase the inflight limit or create more StreamWriter to handle your traffic. */ public static class InflightLimitExceededException extends StatusRuntimeException { private final long currentLimit; private final String writerId; protected InflightLimitExceededException( Status grpcStatus, String writerId, long currentLimit) { super(grpcStatus); this.currentLimit = currentLimit; this.writerId = writerId; } public String getWriterId() { return writerId; } public long getCurrentLimit() { return currentLimit; } } public static class InflightRequestsLimitExceededException extends InflightLimitExceededException { protected InflightRequestsLimitExceededException(String writerId, long currentLimit) { super( Status.fromCode(Status.Code.RESOURCE_EXHAUSTED) .withDescription( "Exceeds client side inflight buffer, consider add more buffer or open more" + " connections. Current limit: " + currentLimit), writerId, currentLimit); } } public static class InflightBytesLimitExceededException extends InflightLimitExceededException { protected InflightBytesLimitExceededException(String writerId, long currentLimit) { super( Status.fromCode(Status.Code.RESOURCE_EXHAUSTED) .withDescription( "Exceeds client side inflight buffer, consider add more buffer or open more" + " connections. Current limit: " + currentLimit), writerId, currentLimit); } } /** * This class is replaced by a generic one. It will be removed soon. Please use {@link * DataHasUnknownFieldException} */ public static final class JsonDataHasUnknownFieldException extends DataHasUnknownFieldException { protected JsonDataHasUnknownFieldException(String jsonFieldName) { super(jsonFieldName); } } /** * Input data object has unknown field to the schema of the SchemaAwareStreamWriter. User can * either turn on IgnoreUnknownFields option on the SchemaAwareStreamWriter, or if they don't want * the error to be ignored, they should recreate the SchemaAwareStreamWriter with the updated * table schema. */ public static class DataHasUnknownFieldException extends IllegalArgumentException { private final String jsonFieldName; public DataHasUnknownFieldException(String jsonFieldName) { super(String.format("The source object has fields unknown to BigQuery: %s.", jsonFieldName)); this.jsonFieldName = jsonFieldName; } public String getFieldName() { return jsonFieldName; } } /** * The connection was shut down because a callback was not received within the maximum wait time. */ public static class MaximumRequestCallbackWaitTimeExceededException extends RuntimeException { private final Duration callbackWaitTime; private final String writerId; private final Duration callbackWaitTimeLimit; public MaximumRequestCallbackWaitTimeExceededException( Duration callbackWaitTime, String writerId, Duration callbackWaitTimeLimit) { super( String.format( "Request has waited in inflight queue for %sms for writer %s, " + "which is over maximum wait time %s", callbackWaitTime, writerId, callbackWaitTimeLimit.toString())); this.callbackWaitTime = callbackWaitTime; this.writerId = writerId; this.callbackWaitTimeLimit = callbackWaitTimeLimit; } public Duration getCallbackWaitTime() { return callbackWaitTime; } public String getWriterId() { return writerId; } public Duration getCallbackWaitTimeLimit() { return callbackWaitTimeLimit; } } private Exceptions() {} }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy