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

org.apache.ratis.thirdparty.io.grpc.Status Maven / Gradle / Ivy

There is a newer version: 1.0.6
Show newest version
/*
 * Copyright 2014 The gRPC Authors
 *
 * 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
 *
 *     http://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 org.apache.ratis.thirdparty.io.grpc;

import static org.apache.ratis.thirdparty.com.google.common.base.Charsets.US_ASCII;
import static org.apache.ratis.thirdparty.com.google.common.base.Charsets.UTF_8;
import static org.apache.ratis.thirdparty.com.google.common.base.Preconditions.checkNotNull;
import static org.apache.ratis.thirdparty.com.google.common.base.Throwables.getStackTraceAsString;

import org.apache.ratis.thirdparty.com.google.common.base.MoreObjects;
import org.apache.ratis.thirdparty.com.google.common.base.Objects;
import org.apache.ratis.thirdparty.io.grpc.Metadata.TrustedAsciiMarshaller;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.TreeMap;
import javax.annotation.CheckReturnValue;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;


/**
 * Defines the status of an operation by providing a standard {@link Code} in conjunction with an
 * optional descriptive message. Instances of {@code Status} are created by starting with the
 * template for the appropriate {@link Status.Code} and supplementing it with additional
 * information: {@code Status.NOT_FOUND.withDescription("Could not find 'important_file.txt'");}
 *
 * 

For clients, every remote call will return a status on completion. In the case of errors this * status may be propagated to blocking stubs as a {@link RuntimeException} or to a listener as an * explicit parameter. * *

Similarly servers can report a status by throwing {@link StatusRuntimeException} * or by passing the status to a callback. * *

Utility functions are provided to convert a status to an exception and to extract them * back out. * *

Extended descriptions, including a list of codes that should not be generated by the library, * can be found at * doc/statuscodes.md */ @Immutable @CheckReturnValue public final class Status { /** * The set of canonical status codes. If new codes are added over time they must choose * a numerical value that does not collide with any previously used value. */ public enum Code { /** * The operation completed successfully. */ OK(0), /** * The operation was cancelled (typically by the caller). */ CANCELLED(1), /** * Unknown error. An example of where this error may be returned is * if a Status value received from another address space belongs to * an error-space that is not known in this address space. Also * errors raised by APIs that do not return enough error information * may be converted to this error. */ UNKNOWN(2), /** * Client specified an invalid argument. Note that this differs * from FAILED_PRECONDITION. INVALID_ARGUMENT indicates arguments * that are problematic regardless of the state of the system * (e.g., a malformed file name). */ INVALID_ARGUMENT(3), /** * Deadline expired before operation could complete. For operations * that change the state of the system, this error may be returned * even if the operation has completed successfully. For example, a * successful response from a server could have been delayed long * enough for the deadline to expire. */ DEADLINE_EXCEEDED(4), /** * Some requested entity (e.g., file or directory) was not found. */ NOT_FOUND(5), /** * Some entity that we attempted to create (e.g., file or directory) already exists. */ ALREADY_EXISTS(6), /** * The caller does not have permission to execute the specified * operation. PERMISSION_DENIED must not be used for rejections * caused by exhausting some resource (use RESOURCE_EXHAUSTED * instead for those errors). PERMISSION_DENIED must not be * used if the caller cannot be identified (use UNAUTHENTICATED * instead for those errors). */ PERMISSION_DENIED(7), /** * Some resource has been exhausted, perhaps a per-user quota, or * perhaps the entire file system is out of space. */ RESOURCE_EXHAUSTED(8), /** * Operation was rejected because the system is not in a state * required for the operation's execution. For example, directory * to be deleted may be non-empty, an rmdir operation is applied to * a non-directory, etc. * *

A litmus test that may help a service implementor in deciding * between FAILED_PRECONDITION, ABORTED, and UNAVAILABLE: * (a) Use UNAVAILABLE if the client can retry just the failing call. * (b) Use ABORTED if the client should retry at a higher-level * (e.g., restarting a read-modify-write sequence). * (c) Use FAILED_PRECONDITION if the client should not retry until * the system state has been explicitly fixed. E.g., if an "rmdir" * fails because the directory is non-empty, FAILED_PRECONDITION * should be returned since the client should not retry unless * they have first fixed up the directory by deleting files from it. */ FAILED_PRECONDITION(9), /** * The operation was aborted, typically due to a concurrency issue * like sequencer check failures, transaction aborts, etc. * *

See litmus test above for deciding between FAILED_PRECONDITION, * ABORTED, and UNAVAILABLE. */ ABORTED(10), /** * Operation was attempted past the valid range. E.g., seeking or * reading past end of file. * *

Unlike INVALID_ARGUMENT, this error indicates a problem that may * be fixed if the system state changes. For example, a 32-bit file * system will generate INVALID_ARGUMENT if asked to read at an * offset that is not in the range [0,2^32-1], but it will generate * OUT_OF_RANGE if asked to read from an offset past the current * file size. * *

There is a fair bit of overlap between FAILED_PRECONDITION and OUT_OF_RANGE. * We recommend using OUT_OF_RANGE (the more specific error) when it applies * so that callers who are iterating through * a space can easily look for an OUT_OF_RANGE error to detect when they are done. */ OUT_OF_RANGE(11), /** * Operation is not implemented or not supported/enabled in this service. */ UNIMPLEMENTED(12), /** * Internal errors. Means some invariants expected by underlying * system has been broken. If you see one of these errors, * something is very broken. */ INTERNAL(13), /** * The service is currently unavailable. This is a most likely a * transient condition and may be corrected by retrying with * a backoff. Note that it is not always safe to retry * non-idempotent operations. * *

See litmus test above for deciding between FAILED_PRECONDITION, * ABORTED, and UNAVAILABLE. */ UNAVAILABLE(14), /** * Unrecoverable data loss or corruption. */ DATA_LOSS(15), /** * The request does not have valid authentication credentials for the * operation. */ UNAUTHENTICATED(16); private final int value; @SuppressWarnings("ImmutableEnumChecker") // we make sure the byte[] can't be modified private final byte[] valueAscii; private Code(int value) { this.value = value; this.valueAscii = Integer.toString(value).getBytes(US_ASCII); } /** * The numerical value of the code. */ public int value() { return value; } /** * Returns a {@link Status} object corresponding to this status code. */ public Status toStatus() { return STATUS_LIST.get(value); } private byte[] valueAscii() { return valueAscii; } } private static final String TEST_EQUALS_FAILURE_PROPERTY = "org.apache.ratis.thirdparty.io.grpc.Status.failOnEqualsForTest"; private static final boolean FAIL_ON_EQUALS_FOR_TEST = Boolean.parseBoolean(System.getProperty(TEST_EQUALS_FAILURE_PROPERTY, "false")); // Create the canonical list of Status instances indexed by their code values. private static final List STATUS_LIST = buildStatusList(); private static List buildStatusList() { TreeMap canonicalizer = new TreeMap<>(); for (Code code : Code.values()) { Status replaced = canonicalizer.put(code.value(), new Status(code)); if (replaced != null) { throw new IllegalStateException("Code value duplication between " + replaced.getCode().name() + " & " + code.name()); } } return Collections.unmodifiableList(new ArrayList<>(canonicalizer.values())); } // A pseudo-enum of Status instances mapped 1:1 with values in Code. This simplifies construction // patterns for derived instances of Status. /** The operation completed successfully. */ public static final Status OK = Code.OK.toStatus(); /** The operation was cancelled (typically by the caller). */ public static final Status CANCELLED = Code.CANCELLED.toStatus(); /** Unknown error. See {@link Code#UNKNOWN}. */ public static final Status UNKNOWN = Code.UNKNOWN.toStatus(); /** Client specified an invalid argument. See {@link Code#INVALID_ARGUMENT}. */ public static final Status INVALID_ARGUMENT = Code.INVALID_ARGUMENT.toStatus(); /** Deadline expired before operation could complete. See {@link Code#DEADLINE_EXCEEDED}. */ public static final Status DEADLINE_EXCEEDED = Code.DEADLINE_EXCEEDED.toStatus(); /** Some requested entity (e.g., file or directory) was not found. */ public static final Status NOT_FOUND = Code.NOT_FOUND.toStatus(); /** Some entity that we attempted to create (e.g., file or directory) already exists. */ public static final Status ALREADY_EXISTS = Code.ALREADY_EXISTS.toStatus(); /** * The caller does not have permission to execute the specified operation. See {@link * Code#PERMISSION_DENIED}. */ public static final Status PERMISSION_DENIED = Code.PERMISSION_DENIED.toStatus(); /** The request does not have valid authentication credentials for the operation. */ public static final Status UNAUTHENTICATED = Code.UNAUTHENTICATED.toStatus(); /** * Some resource has been exhausted, perhaps a per-user quota, or perhaps the entire file system * is out of space. */ public static final Status RESOURCE_EXHAUSTED = Code.RESOURCE_EXHAUSTED.toStatus(); /** * Operation was rejected because the system is not in a state required for the operation's * execution. See {@link Code#FAILED_PRECONDITION}. */ public static final Status FAILED_PRECONDITION = Code.FAILED_PRECONDITION.toStatus(); /** * The operation was aborted, typically due to a concurrency issue like sequencer check failures, * transaction aborts, etc. See {@link Code#ABORTED}. */ public static final Status ABORTED = Code.ABORTED.toStatus(); /** Operation was attempted past the valid range. See {@link Code#OUT_OF_RANGE}. */ public static final Status OUT_OF_RANGE = Code.OUT_OF_RANGE.toStatus(); /** Operation is not implemented or not supported/enabled in this service. */ public static final Status UNIMPLEMENTED = Code.UNIMPLEMENTED.toStatus(); /** Internal errors. See {@link Code#INTERNAL}. */ public static final Status INTERNAL = Code.INTERNAL.toStatus(); /** The service is currently unavailable. See {@link Code#UNAVAILABLE}. */ public static final Status UNAVAILABLE = Code.UNAVAILABLE.toStatus(); /** Unrecoverable data loss or corruption. */ public static final Status DATA_LOSS = Code.DATA_LOSS.toStatus(); /** * Return a {@link Status} given a canonical error {@link Code} value. */ public static Status fromCodeValue(int codeValue) { if (codeValue < 0 || codeValue > STATUS_LIST.size()) { return UNKNOWN.withDescription("Unknown code " + codeValue); } else { return STATUS_LIST.get(codeValue); } } private static Status fromCodeValue(byte[] asciiCodeValue) { if (asciiCodeValue.length == 1 && asciiCodeValue[0] == '0') { return Status.OK; } return fromCodeValueSlow(asciiCodeValue); } @SuppressWarnings("fallthrough") private static Status fromCodeValueSlow(byte[] asciiCodeValue) { int index = 0; int codeValue = 0; switch (asciiCodeValue.length) { case 2: if (asciiCodeValue[index] < '0' || asciiCodeValue[index] > '9') { break; } codeValue += (asciiCodeValue[index++] - '0') * 10; // fall through case 1: if (asciiCodeValue[index] < '0' || asciiCodeValue[index] > '9') { break; } codeValue += asciiCodeValue[index] - '0'; if (codeValue < STATUS_LIST.size()) { return STATUS_LIST.get(codeValue); } break; default: break; } return UNKNOWN.withDescription("Unknown code " + new String(asciiCodeValue, US_ASCII)); } /** * Return a {@link Status} given a canonical error {@link Code} object. */ public static Status fromCode(Code code) { return code.toStatus(); } /** * Key to bind status code to trailing metadata. */ static final Metadata.Key CODE_KEY = Metadata.Key.of("grpc-status", false /* not pseudo */, new StatusCodeMarshaller()); /** * Marshals status messages for ({@link #MESSAGE_KEY}. gRPC does not use binary coding of * status messages by default, which makes sending arbitrary strings difficult. This marshaller * uses ASCII printable characters by default, and percent encodes (e.g. %0A) all non ASCII bytes. * This leads to normal text being mostly readable (especially useful for debugging), and special * text still being sent. * *

By default, the HTTP spec says that header values must be encoded using a strict subset of * ASCII (See RFC 7230 section 3.2.6). HTTP/2 HPACK allows use of arbitrary binary headers, but * we do not use them for interoperating with existing HTTP/1.1 code. Since the grpc-message * is encoded to such a header, it needs to not use forbidden characters. * *

This marshaller works by converting the passed in string into UTF-8, checking to see if * each individual byte is an allowable byte, and then either percent encoding or passing it * through. When percent encoding, the byte is converted into hexadecimal notation with a '%' * prepended. * *

When unmarshalling, bytes are passed through unless they match the "%XX" pattern. If they * do match, the unmarshaller attempts to convert them back into their original UTF-8 byte * sequence. After the input header bytes are converted into UTF-8 bytes, the new byte array is * reinterpretted back as a string. */ private static final TrustedAsciiMarshaller STATUS_MESSAGE_MARSHALLER = new StatusMessageMarshaller(); /** * Key to bind status message to trailing metadata. */ static final Metadata.Key MESSAGE_KEY = Metadata.Key.of("grpc-message", false /* not pseudo */, STATUS_MESSAGE_MARSHALLER); /** * Extract an error {@link Status} from the causal chain of a {@link Throwable}. * If no status can be found, a status is created with {@link Code#UNKNOWN} as its code and * {@code t} as its cause. * * @return non-{@code null} status */ public static Status fromThrowable(Throwable t) { Throwable cause = checkNotNull(t, "t"); while (cause != null) { if (cause instanceof StatusException) { return ((StatusException) cause).getStatus(); } else if (cause instanceof StatusRuntimeException) { return ((StatusRuntimeException) cause).getStatus(); } cause = cause.getCause(); } // Couldn't find a cause with a Status return UNKNOWN.withCause(t); } /** * Extract an error trailers from the causal chain of a {@link Throwable}. * * @return the trailers or {@code null} if not found. */ @Nullable @ExperimentalApi("https://github.com/grpc/grpc-java/issues/4683") public static Metadata trailersFromThrowable(Throwable t) { Throwable cause = checkNotNull(t, "t"); while (cause != null) { if (cause instanceof StatusException) { return ((StatusException) cause).getTrailers(); } else if (cause instanceof StatusRuntimeException) { return ((StatusRuntimeException) cause).getTrailers(); } cause = cause.getCause(); } return null; } static String formatThrowableMessage(Status status) { if (status.description == null) { return status.code.toString(); } else { return status.code + ": " + status.description; } } private final Code code; private final String description; private final Throwable cause; private Status(Code code) { this(code, null, null); } private Status(Code code, @Nullable String description, @Nullable Throwable cause) { this.code = checkNotNull(code, "code"); this.description = description; this.cause = cause; } /** * Create a derived instance of {@link Status} with the given cause. * However, the cause is not transmitted from server to client. */ public Status withCause(Throwable cause) { if (Objects.equal(this.cause, cause)) { return this; } return new Status(this.code, this.description, cause); } /** * Create a derived instance of {@link Status} with the given description. Leading and trailing * whitespace may be removed; this may change in the future. */ public Status withDescription(String description) { if (Objects.equal(this.description, description)) { return this; } return new Status(this.code, description, this.cause); } /** * Create a derived instance of {@link Status} augmenting the current description with * additional detail. Leading and trailing whitespace may be removed; this may change in the * future. */ public Status augmentDescription(String additionalDetail) { if (additionalDetail == null) { return this; } else if (this.description == null) { return new Status(this.code, additionalDetail, this.cause); } else { return new Status(this.code, this.description + "\n" + additionalDetail, this.cause); } } /** * The canonical status code. */ public Code getCode() { return code; } /** * A description of this status for human consumption. */ @Nullable public String getDescription() { return description; } /** * The underlying cause of an error. * Note that the cause is not transmitted from server to client. */ @Nullable public Throwable getCause() { return cause; } /** * Is this status OK, i.e., not an error. */ public boolean isOk() { return Code.OK == code; } /** * Convert this {@link Status} to a {@link RuntimeException}. Use {@link #fromThrowable} * to recover this {@link Status} instance when the returned exception is in the causal chain. */ public StatusRuntimeException asRuntimeException() { return new StatusRuntimeException(this); } /** * Same as {@link #asRuntimeException()} but includes the provided trailers in the returned * exception. */ @ExperimentalApi("https://github.com/grpc/grpc-java/issues/4683") public StatusRuntimeException asRuntimeException(@Nullable Metadata trailers) { return new StatusRuntimeException(this, trailers); } /** * Convert this {@link Status} to an {@link Exception}. Use {@link #fromThrowable} * to recover this {@link Status} instance when the returned exception is in the causal chain. */ public StatusException asException() { return new StatusException(this); } /** * Same as {@link #asException()} but includes the provided trailers in the returned exception. */ public StatusException asException(@Nullable Metadata trailers) { return new StatusException(this, trailers); } /** A string representation of the status useful for debugging. */ @Override public String toString() { return MoreObjects.toStringHelper(this) .add("code", code.name()) .add("description", description) .add("cause", cause != null ? getStackTraceAsString(cause) : cause) .toString(); } private static final class StatusCodeMarshaller implements TrustedAsciiMarshaller { @Override public byte[] toAsciiString(Status status) { return status.getCode().valueAscii(); } @Override public Status parseAsciiString(byte[] serialized) { return fromCodeValue(serialized); } } private static final class StatusMessageMarshaller implements TrustedAsciiMarshaller { private static final byte[] HEX = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; @Override public byte[] toAsciiString(String value) { byte[] valueBytes = value.getBytes(UTF_8); for (int i = 0; i < valueBytes.length; i++) { byte b = valueBytes[i]; // If there are only non escaping characters, skip the slow path. if (isEscapingChar(b)) { return toAsciiStringSlow(valueBytes, i); } } return valueBytes; } private static boolean isEscapingChar(byte b) { return b < ' ' || b >= '~' || b == '%'; } /** * @param valueBytes the UTF-8 bytes * @param ri The reader index, pointed at the first byte that needs escaping. */ private static byte[] toAsciiStringSlow(byte[] valueBytes, int ri) { byte[] escapedBytes = new byte[ri + (valueBytes.length - ri) * 3]; // copy over the good bytes if (ri != 0) { System.arraycopy(valueBytes, 0, escapedBytes, 0, ri); } int wi = ri; for (; ri < valueBytes.length; ri++) { byte b = valueBytes[ri]; // Manually implement URL encoding, per the gRPC spec. if (isEscapingChar(b)) { escapedBytes[wi] = '%'; escapedBytes[wi + 1] = HEX[(b >> 4) & 0xF]; escapedBytes[wi + 2] = HEX[b & 0xF]; wi += 3; continue; } escapedBytes[wi++] = b; } return Arrays.copyOf(escapedBytes, wi); } @SuppressWarnings("deprecation") // Use fast but deprecated String ctor @Override public String parseAsciiString(byte[] value) { for (int i = 0; i < value.length; i++) { byte b = value[i]; if (b < ' ' || b >= '~' || (b == '%' && i + 2 < value.length)) { return parseAsciiStringSlow(value); } } return new String(value, 0); } private static String parseAsciiStringSlow(byte[] value) { ByteBuffer buf = ByteBuffer.allocate(value.length); for (int i = 0; i < value.length;) { if (value[i] == '%' && i + 2 < value.length) { try { buf.put((byte)Integer.parseInt(new String(value, i + 1, 2, US_ASCII), 16)); i += 3; continue; } catch (NumberFormatException e) { // ignore, fall through, just push the bytes. } } buf.put(value[i]); i += 1; } return new String(buf.array(), 0, buf.position(), UTF_8); } } /** * Equality on Statuses is not well defined. Instead, do comparison based on their Code with * {@link #getCode}. The description and cause of the Status are unlikely to be stable, and * additional fields may be added to Status in the future. */ @Override public boolean equals(Object obj) { assert !FAIL_ON_EQUALS_FOR_TEST : "Status.equals called; disable this by setting " + TEST_EQUALS_FAILURE_PROPERTY; return super.equals(obj); } /** * Hash codes on Statuses are not well defined. * * @see #equals */ @Override public int hashCode() { return super.hashCode(); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy