diff --git a/benchmarks/src/jmh/java/io/grpc/StatusBenchmark.java b/benchmarks/src/jmh/java/io/grpc/StatusBenchmark.java new file mode 100644 index 00000000000..b4a573a665c --- /dev/null +++ b/benchmarks/src/jmh/java/io/grpc/StatusBenchmark.java @@ -0,0 +1,110 @@ +/* + * Copyright 2016, Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.grpc; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; + +import java.nio.charset.Charset; +import java.util.concurrent.TimeUnit; + +/** StatusBenchmark. */ +@State(Scope.Benchmark) +public class StatusBenchmark { + + /** + * Javadoc comment. + */ + @Benchmark + @BenchmarkMode(Mode.SampleTime) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + public byte[] messageEncodePlain() { + return Status.MESSAGE_KEY.toBytes("Unexpected RST in stream"); + } + + /** + * Javadoc comment. + */ + @Benchmark + @BenchmarkMode(Mode.SampleTime) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + public byte[] messageEncodeEscape() { + return Status.MESSAGE_KEY.toBytes("Some Error\nWasabi and Horseradish are the same"); + } + + /** + * Javadoc comment. + */ + @Benchmark + @BenchmarkMode(Mode.SampleTime) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + public String messageDecodePlain() { + return Status.MESSAGE_KEY.parseBytes( + "Unexpected RST in stream".getBytes(Charset.forName("US-ASCII"))); + } + + /** + * Javadoc comment. + */ + @Benchmark + @BenchmarkMode(Mode.SampleTime) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + public String messageDecodeEscape() { + return Status.MESSAGE_KEY.parseBytes( + "Some Error%10Wasabi and Horseradish are the same".getBytes(Charset.forName("US-ASCII"))); + } + + /** + * Javadoc comment. + */ + @Benchmark + @BenchmarkMode(Mode.SampleTime) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + public byte[] codeEncode() { + return Status.CODE_KEY.toBytes(Status.DATA_LOSS); + } + + /** + * Javadoc comment. + */ + @Benchmark + @BenchmarkMode(Mode.SampleTime) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + public Status codeDecode() { + return Status.CODE_KEY.parseBytes("15".getBytes(Charset.forName("US-ASCII"))); + } +} + diff --git a/core/src/main/java/io/grpc/InternalMetadata.java b/core/src/main/java/io/grpc/InternalMetadata.java new file mode 100644 index 00000000000..7c35c092eb9 --- /dev/null +++ b/core/src/main/java/io/grpc/InternalMetadata.java @@ -0,0 +1,80 @@ +/* + * Copyright 2016, Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.grpc; + +import io.grpc.Metadata.Key; + +import java.nio.charset.Charset; + +/** + * Internal {@link Metadata} accessor. This is intended for use by io.grpc.internal, and the + * specifically supported transport packages. If you *really* think you need to use this, contact + * the gRPC team first. + */ +@Internal +public final class InternalMetadata { + + /** + * A specialized plain ASCII marshaller. Both input and output are assumed to be valid header + * ASCII. + */ + @Internal + public interface TrustedAsciiMarshaller { + /** + * Serialize a metadata value to a ASCII string that contains only the characters listed in the + * class comment of {@link io.grpc.Metadata.AsciiMarshaller}. Otherwise the output may be + * considered invalid and discarded by the transport, or the call may fail. + * + * @param value to serialize + * @return serialized version of value, or null if value cannot be transmitted. + */ + byte[] toAsciiString(T value); + + /** + * Parse a serialized metadata value from an ASCII string. + * + * @param serialized value of metadata to parse + * @return a parsed instance of type T + */ + T parseAsciiString(byte[] serialized); + } + + /** + * Copy of StandardCharsets, which is only available on Java 1.7 and above. + */ + public static final Charset US_ASCII = Charset.forName("US-ASCII"); + + @Internal + public static Key keyOf(String name, TrustedAsciiMarshaller marshaller) { + return Metadata.Key.of(name, marshaller); + } +} diff --git a/core/src/main/java/io/grpc/Metadata.java b/core/src/main/java/io/grpc/Metadata.java index bfa9603b534..2833343dc82 100644 --- a/core/src/main/java/io/grpc/Metadata.java +++ b/core/src/main/java/io/grpc/Metadata.java @@ -38,6 +38,8 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; +import io.grpc.InternalMetadata.TrustedAsciiMarshaller; + import java.util.ArrayList; import java.util.Arrays; import java.util.BitSet; @@ -460,6 +462,10 @@ public static Key of(String name, AsciiMarshaller marshaller) { return new AsciiKey(name, marshaller); } + static Key of(String name, TrustedAsciiMarshaller marshaller) { + return new TrustedAsciiKey(name, marshaller); + } + private final String originalName; private final String name; @@ -603,7 +609,7 @@ private AsciiKey(String name, AsciiMarshaller marshaller) { super(name); Preconditions.checkArgument( !name.endsWith(BINARY_HEADER_SUFFIX), - "ASCII header is named %s. It must not end with %s", + "ASCII header is named %s. Only binary headers may end with %s", name, BINARY_HEADER_SUFFIX); this.marshaller = Preconditions.checkNotNull(marshaller, "marshaller"); } @@ -619,6 +625,32 @@ T parseBytes(byte[] serialized) { } } + private static final class TrustedAsciiKey extends Key { + private final TrustedAsciiMarshaller marshaller; + + /** + * Keys have a name and an ASCII marshaller used for serialization. + */ + private TrustedAsciiKey(String name, TrustedAsciiMarshaller marshaller) { + super(name); + Preconditions.checkArgument( + !name.endsWith(BINARY_HEADER_SUFFIX), + "ASCII header is named %s. Only binary headers may end with %s", + name, BINARY_HEADER_SUFFIX); + this.marshaller = Preconditions.checkNotNull(marshaller, "marshaller"); + } + + @Override + byte[] toBytes(T value) { + return marshaller.toAsciiString(value); + } + + @Override + T parseBytes(byte[] serialized) { + return marshaller.parseAsciiString(serialized); + } + } + private static class MetadataEntry { Object parsed; diff --git a/core/src/main/java/io/grpc/Status.java b/core/src/main/java/io/grpc/Status.java index 630d8a6ff09..b7be3802da2 100644 --- a/core/src/main/java/io/grpc/Status.java +++ b/core/src/main/java/io/grpc/Status.java @@ -36,7 +36,7 @@ import com.google.common.base.MoreObjects; import com.google.common.base.Objects; -import io.grpc.Metadata.AsciiMarshaller; +import io.grpc.InternalMetadata.TrustedAsciiMarshaller; import java.nio.ByteBuffer; import java.nio.charset.Charset; @@ -215,11 +215,11 @@ public enum Code { UNAUTHENTICATED(16); private final int value; - private final String valueAscii; + private final byte[] valueAscii; private Code(int value) { this.value = value; - this.valueAscii = Integer.toString(value); + this.valueAscii = Integer.toString(value).getBytes(US_ASCII); } /** @@ -233,11 +233,14 @@ public Status toStatus() { return STATUS_LIST.get(value); } - private String valueAscii() { + private byte[] valueAscii() { return valueAscii; } } + private static final Charset US_ASCII = Charset.forName("US-ASCII"); + private static final Charset UTF_8 = Charset.forName("UTF-8"); + // Create the canonical list of Status instances indexed by their code values. private static final List STATUS_LIST = buildStatusList(); @@ -314,6 +317,39 @@ public static Status fromCodeValue(int 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. */ @@ -350,53 +386,15 @@ public static Status fromCode(Code code) { * 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 AsciiMarshaller STATUS_MESSAGE_MARSHALLER = - new AsciiMarshaller() { - - @Override - public String toAsciiString(String value) { - // This can be made faster if necessary. - StringBuilder sb = new StringBuilder(value.length()); - for (byte b : value.getBytes(Charset.forName("UTF-8"))) { - if (b >= ' ' && b < '%' || b > '%' && b < '~') { - // fast path, if it's plain ascii and not a percent, pass it through. - sb.append((char) b); - } else { - sb.append(String.format("%%%02X", b)); - } - } - return sb.toString(); - } - - @Override - public String parseAsciiString(String value) { - Charset transerEncoding = Charset.forName("US-ASCII"); - // This can be made faster if necessary. - byte[] source = value.getBytes(transerEncoding); - ByteBuffer buf = ByteBuffer.allocate(source.length); - for (int i = 0; i < source.length; ) { - if (source[i] == '%' && i + 2 < source.length) { - try { - buf.put((byte)Integer.parseInt(new String(source, i + 1, 2, transerEncoding), 16)); - i += 3; - continue; - } catch (NumberFormatException e) { - // ignore, fall through, just push the bytes. - } - } - buf.put(source[i]); - i += 1; - } - return new String(buf.array(), 0, buf.position(), Charset.forName("UTF-8")); - } - }; + private static final InternalMetadata.TrustedAsciiMarshaller STATUS_MESSAGE_MARSHALLER = + new StatusMessageMarshaller(); /** * Key to bind status message to trailing metadata. */ @Internal - public static final Metadata.Key MESSAGE_KEY - = Metadata.Key.of("grpc-message", STATUS_MESSAGE_MARSHALLER); + public static final Metadata.Key MESSAGE_KEY = + Metadata.Key.of("grpc-message", STATUS_MESSAGE_MARSHALLER); /** * Extract an error {@link Status} from the causal chain of a {@link Throwable}. @@ -571,15 +569,97 @@ public String toString() { .toString(); } - private static class StatusCodeMarshaller implements Metadata.AsciiMarshaller { + private static final class StatusCodeMarshaller implements TrustedAsciiMarshaller { @Override - public String toAsciiString(Status status) { + public byte[] toAsciiString(Status status) { return status.getCode().valueAscii(); } @Override - public Status parseAsciiString(String serialized) { - return fromCodeValue(Integer.valueOf(serialized)); + 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; + } + byte[] dest = new byte[wi]; + System.arraycopy(escapedBytes, 0, dest, 0, wi); + + return dest; + } + + @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); } } diff --git a/core/src/main/java/io/grpc/internal/Http2ClientStream.java b/core/src/main/java/io/grpc/internal/Http2ClientStream.java index 63a4be8c3c0..9cafee49606 100644 --- a/core/src/main/java/io/grpc/internal/Http2ClientStream.java +++ b/core/src/main/java/io/grpc/internal/Http2ClientStream.java @@ -34,6 +34,7 @@ import com.google.common.base.Charsets; import com.google.common.base.Preconditions; +import io.grpc.InternalMetadata; import io.grpc.Metadata; import io.grpc.Status; @@ -49,21 +50,30 @@ public abstract class Http2ClientStream extends AbstractClientStream { /** * Metadata marshaller for HTTP status lines. */ - private static final Metadata.AsciiMarshaller HTTP_STATUS_LINE_MARSHALLER = - new Metadata.AsciiMarshaller() { + private static final InternalMetadata.TrustedAsciiMarshaller HTTP_STATUS_MARSHALLER = + new InternalMetadata.TrustedAsciiMarshaller() { @Override - public String toAsciiString(Integer value) { - return value.toString(); + public byte[] toAsciiString(Integer value) { + throw new UnsupportedOperationException(); } + /** + * RFC 7231 says status codes are 3 digits long. + * + * @see: RFC 7231 + */ @Override - public Integer parseAsciiString(String serialized) { - return Integer.parseInt(serialized.split(" ", 2)[0]); + public Integer parseAsciiString(byte[] serialized) { + if (serialized.length >= 3) { + return (serialized[0] - '0') * 100 + (serialized[1] - '0') * 10 + (serialized[2] - '0'); + } + throw new NumberFormatException( + "Malformed status code " + new String(serialized, InternalMetadata.US_ASCII)); } }; - private static final Metadata.Key HTTP2_STATUS = Metadata.Key.of(":status", - HTTP_STATUS_LINE_MARSHALLER); + private static final Metadata.Key HTTP2_STATUS = InternalMetadata.keyOf(":status", + HTTP_STATUS_MARSHALLER); /** When non-{@code null}, {@link #transportErrorMetadata} must also be non-{@code null}. */ private Status transportError;