core: change Metadata internals to avoid garbage creation

Before:
Benchmark                                                                   (headerCount)    Mode     Cnt        Score    Error  Units
InboundHeadersBenchmark.defaultHeaders_clientHandler                                  N/A    avgt      10      240.879 ±  4.903  ns/op
InboundHeadersBenchmark.defaultHeaders_serverHandler                                  N/A    avgt      10      882.354 ± 16.177  ns/op
InboundHeadersBenchmark.grpcHeaders_clientHandler                                     N/A    avgt      10      208.068 ±  5.380  ns/op
InboundHeadersBenchmark.grpcHeaders_serverHandler                                     N/A    avgt      10      477.604 ±  8.200  ns/op
OutboundHeadersBenchmark.convertClientHeaders                                           1  sample  234233      125.232 ± 11.903  ns/op
OutboundHeadersBenchmark.convertClientHeaders                                           5  sample  344367      264.343 ± 18.318  ns/op
OutboundHeadersBenchmark.convertClientHeaders                                          10  sample  392273      439.640 ±  2.589  ns/op
OutboundHeadersBenchmark.convertClientHeaders                                          20  sample  221506      855.115 ± 38.899  ns/op
OutboundHeadersBenchmark.convertServerHeaders                                           1  sample  253676      111.941 ±  2.742  ns/op
OutboundHeadersBenchmark.convertServerHeaders                                           5  sample  368499      248.255 ±  2.601  ns/op
OutboundHeadersBenchmark.convertServerHeaders                                          10  sample  390015      439.651 ± 11.040  ns/op
OutboundHeadersBenchmark.convertServerHeaders                                          20  sample  221807      840.435 ± 21.667  ns/op
OutboundHeadersBenchmark.encodeClientHeaders                                            1  sample  230139      432.866 ± 25.503  ns/op
OutboundHeadersBenchmark.encodeClientHeaders                                            5  sample  226901      765.095 ± 19.969  ns/op
OutboundHeadersBenchmark.encodeClientHeaders                                           10  sample  260495     1268.239 ± 21.850  ns/op
OutboundHeadersBenchmark.encodeClientHeaders                                           20  sample  311526     2059.973 ± 23.503  ns/op

After:

Benchmark                                                                   (headerCount)    Mode     Cnt        Score    Error  Units
InboundHeadersBenchmark.defaultHeaders_clientHandler                                  N/A    avgt      10      104.317 ±  1.973  ns/op
InboundHeadersBenchmark.defaultHeaders_serverHandler                                  N/A    avgt      10      395.666 ± 11.056  ns/op
InboundHeadersBenchmark.grpcHeaders_clientHandler                                     N/A    avgt      10       64.147 ±  4.076  ns/op
InboundHeadersBenchmark.grpcHeaders_serverHandler                                     N/A    avgt      10      228.299 ±  2.874  ns/op
OutboundHeadersBenchmark.convertClientHeaders                                           1  sample  252451      102.718 ±  2.714  ns/op
OutboundHeadersBenchmark.convertClientHeaders                                           5  sample  239976      225.812 ± 38.824  ns/op
OutboundHeadersBenchmark.convertClientHeaders                                          10  sample  258119      364.475 ± 57.217  ns/op
OutboundHeadersBenchmark.convertClientHeaders                                          20  sample  260138      676.950 ± 36.243  ns/op
OutboundHeadersBenchmark.convertServerHeaders                                           1  sample  276064      105.371 ±  1.859  ns/op
OutboundHeadersBenchmark.convertServerHeaders                                           5  sample  255128      190.970 ± 16.475  ns/op
OutboundHeadersBenchmark.convertServerHeaders                                          10  sample  272923      366.769 ± 28.204  ns/op
OutboundHeadersBenchmark.convertServerHeaders                                          20  sample  264797      641.961 ± 18.879  ns/op
OutboundHeadersBenchmark.encodeClientHeaders                                            1  sample  226078      425.262 ±  3.481  ns/op
OutboundHeadersBenchmark.encodeClientHeaders                                            5  sample  253606      675.488 ± 26.001  ns/op
OutboundHeadersBenchmark.encodeClientHeaders                                           10  sample  286556     1157.014 ± 12.923  ns/op
OutboundHeadersBenchmark.encodeClientHeaders                                           20  sample  345649     1874.806 ± 36.227  ns/op
This commit is contained in:
Carl Mastrangelo 2016-08-31 17:42:19 -07:00
parent 16c07ba434
commit 141eed5ed0
8 changed files with 405 additions and 329 deletions

View File

@ -71,10 +71,27 @@ public final class InternalMetadata {
/**
* Copy of StandardCharsets, which is only available on Java 1.7 and above.
*/
@Internal
public static final Charset US_ASCII = Charset.forName("US-ASCII");
@Internal
public static <T> Key<T> keyOf(String name, TrustedAsciiMarshaller<T> marshaller) {
return Metadata.Key.of(name, marshaller);
}
@Internal
public static Metadata newMetadata(byte[]... binaryValues) {
return new Metadata(binaryValues);
}
@Internal
public static Metadata newMetadata(int usedNames, byte[]... binaryValues) {
return new Metadata(usedNames, binaryValues);
}
@Internal
public static byte[][] serialize(Metadata md) {
return md.serialize();
}
}

View File

@ -37,28 +37,32 @@ import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.io.BaseEncoding;
import io.grpc.InternalMetadata.TrustedAsciiMarshaller;
import java.util.ArrayList;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe;
/**
* Provides access to read and write metadata values to be exchanged during a call.
* <p>
* This class is not thread safe, implementations should ensure that header reads and writes
* do not occur in multiple threads concurrently.
* </p>
*
* <p>This class is not thread safe, implementations should ensure that header reads and writes do
* not occur in multiple threads concurrently.
*/
@NotThreadSafe
public final class Metadata {
@ -75,11 +79,11 @@ public final class Metadata {
*
* <p>This should be used when raw bytes are favored over un-serialized version of object. Can be
* helpful in situations where more processing to bytes is needed on application side, avoids
* double encoding/decoding.</p>
* double encoding/decoding.
*
* <p>Both {@link BinaryMarshaller#toBytes} and {@link BinaryMarshaller#parseBytes} methods do
* not return a copy of the byte array. Do _not_ modify the byte arrays of either the arguments
* or return values.</p>
* <p>Both {@link BinaryMarshaller#toBytes} and {@link BinaryMarshaller#parseBytes} methods do not
* return a copy of the byte array. Do _not_ modify the byte arrays of either the arguments or
* return values.
*/
public static final BinaryMarshaller<byte[]> BINARY_BYTE_MARSHALLER =
new BinaryMarshaller<byte[]>() {
@ -116,10 +120,9 @@ public final class Metadata {
}
};
/**
* Simple metadata marshaller that encodes an integer as a signed decimal string.
*/
static final AsciiMarshaller<Integer> INTEGER_MARSHALLER = new AsciiMarshaller<Integer>() {
/** Simple metadata marshaller that encodes an integer as a signed decimal string. */
static final AsciiMarshaller<Integer> INTEGER_MARSHALLER =
new AsciiMarshaller<Integer>() {
@Override
public String toAsciiString(Integer value) {
@ -132,87 +135,149 @@ public final class Metadata {
}
};
/** All value lists can be added to. No value list may be empty. */
// Use LinkedHashMap for consistent ordering for tests.
private final Map<String, List<MetadataEntry>> store =
new LinkedHashMap<String, List<MetadataEntry>>();
/** The number of headers stored by this metadata. */
private int storeCount;
/**
* Constructor called by the transport layer when it receives binary metadata.
* Constructor called by the transport layer when it receives binary metadata. Metadata will
* mutate the passed in array.
*/
// TODO(louiscryan): Convert to use ByteString so we can cache transformations
@Internal
public Metadata(byte[]... binaryValues) {
checkArgument(binaryValues.length % 2 == 0,
"Odd number of key-value pairs: %s", binaryValues.length);
for (int i = 0; i < binaryValues.length; i += 2) {
// The transport might provide an array with null values at the end.
if (binaryValues[i] == null) {
break;
}
String name = new String(binaryValues[i], US_ASCII);
storeAdd(name, new MetadataEntry(name.endsWith(BINARY_HEADER_SUFFIX), binaryValues[i + 1]));
}
Metadata(byte[]... binaryValues) {
this(binaryValues.length / 2, binaryValues);
}
/**
* Constructor called by the application layer when it wants to send metadata.
* Constructor called by the transport layer when it receives binary metadata. Metadata will
* mutate the passed in array.
*
* @param usedNames the number of
*/
Metadata(int usedNames, byte[]... binaryValues) {
assert (binaryValues.length & 1) == 0 : "Odd number of key-value pairs " + binaryValues.length;
size = usedNames;
namesAndValues = binaryValues;
}
private byte[][] namesAndValues;
// The unscaled number of headers present.
private int size;
private byte[] name(int i) {
return namesAndValues[i * 2];
}
private void name(int i, byte[] name) {
namesAndValues[i * 2] = name;
}
private byte[] value(int i) {
return namesAndValues[i * 2 + 1];
}
private void value(int i, byte[] value) {
namesAndValues[i * 2 + 1] = value;
}
private int cap() {
return namesAndValues != null ? namesAndValues.length : 0;
}
// The scaled version of size.
private int len() {
return size * 2;
}
private boolean isEmpty() {
/** checks when {@link #namesAndValues} is null or has no elements */
return size == 0;
}
/** Constructor called by the application layer when it wants to send metadata. */
public Metadata() {}
private void storeAdd(String name, MetadataEntry value) {
List<MetadataEntry> values = store.get(name);
if (values == null) {
// We expect there to be usually unique header values, so prefer smaller arrays.
values = new ArrayList<MetadataEntry>(1);
store.put(name, values);
}
storeCount++;
values.add(value);
}
/**
* Returns the total number of key-value headers in this metadata, including duplicates.
*/
/** Returns the total number of key-value headers in this metadata, including duplicates. */
@Internal
public int headerCount() {
return storeCount;
return size;
}
/**
* Returns true if a value is defined for the given key.
*/
/** Returns true if a value is defined for the given key. */
public boolean containsKey(Key<?> key) {
return store.containsKey(key.name());
for (int i = 0; i < size; i++) {
if (bytesEqual(key.asciiName(), name(i))) {
return true;
}
}
return false;
}
/**
* Returns the last metadata entry added with the name 'name' parsed as T.
*
* @return the parsed metadata entry or null if there are none.
*/
public <T> T get(Key<T> key) {
List<MetadataEntry> values = store.get(key.name());
if (values == null) {
for (int i = size - 1; i >= 0; i--) {
if (bytesEqual(key.asciiName(), name(i))) {
return key.parseBytes(value(i));
}
}
return null;
}
MetadataEntry metadataEntry = values.get(values.size() - 1);
return metadataEntry.getParsed(key);
private final class IterableAt<T> implements Iterable<T> {
private final Key<T> key;
private int startIdx;
private IterableAt(Key<T> key, int startIdx) {
this.key = key;
this.startIdx = startIdx;
}
@Override
public Iterator<T> iterator() {
return new Iterator<T>() {
private boolean hasNext = true;
private int idx = startIdx;
@Override
public boolean hasNext() {
if (hasNext) {
return true;
}
for (; idx < size; idx++) {
if (bytesEqual(key.asciiName(), name(idx))) {
hasNext = true;
return hasNext;
}
}
return false;
}
@Override
public T next() {
if (hasNext()) {
hasNext = false;
return key.parseBytes(value(idx++));
}
throw new NoSuchElementException();
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
};
}
}
/**
* Returns all the metadata entries named 'name', in the order they were received,
* parsed as T or null if there are none. The iterator is not guaranteed to be "live." It may or
* may not be accurate if Metadata is mutated.
* Returns all the metadata entries named 'name', in the order they were received, parsed as T or
* null if there are none. The iterator is not guaranteed to be "live." It may or may not be
* accurate if Metadata is mutated.
*/
public <T> Iterable<T> getAll(Key<T> key) {
if (containsKey(key)) {
/* This is unmodifiable currently, but could be made to support remove() in the future. If
* removal support is added, the {@link #storeCount} variable needs to be updated
* appropriately. */
return new ValueIterable<T>(key, store.get(key.name()));
public <T> Iterable<T> getAll(final Key<T> key) {
for (int i = 0; i < size; i++) {
if (bytesEqual(key.asciiName(), name(i))) {
return new IterableAt<T>(key, i);
}
}
return null;
}
@ -222,8 +287,17 @@ public final class Metadata {
*
* @return unmodifiable Set of keys
*/
@SuppressWarnings("deprecation") // The String ctor is deprecated, but fast.
public Set<String> keys() {
return Collections.unmodifiableSet(store.keySet());
if (isEmpty()) {
return Collections.emptySet();
}
Set<String> ks = new HashSet<String>(size);
for (int i = 0; i < size; i++) {
ks.add(new String(name(i), 0 /* hibyte */));
}
// immutable in case we decide to change the implementation later.
return Collections.unmodifiableSet(ks);
}
/**
@ -235,7 +309,25 @@ public final class Metadata {
public <T> void put(Key<T> key, T value) {
Preconditions.checkNotNull(key, "key");
Preconditions.checkNotNull(value, "value");
storeAdd(key.name, new MetadataEntry(key, value));
maybeExpand();
name(size, key.asciiName());
value(size, key.toBytes(value));
size++;
}
private void maybeExpand() {
if (len() == 0 || len() == cap()) {
expand(Math.max(len() * 2, 8));
}
}
// Expands to exactly the desired capacity.
private void expand(int newCapacity) {
byte[][] newNamesAndValues = new byte[newCapacity][];
if (!isEmpty()) {
System.arraycopy(namesAndValues, 0, newNamesAndValues, 0, len());
}
namesAndValues = newNamesAndValues;
}
/**
@ -249,32 +341,50 @@ public final class Metadata {
public <T> boolean remove(Key<T> key, T value) {
Preconditions.checkNotNull(key, "key");
Preconditions.checkNotNull(value, "value");
List<MetadataEntry> values = store.get(key.name());
if (values == null) {
return false;
}
for (int i = 0; i < values.size(); i++) {
MetadataEntry entry = values.get(i);
if (!value.equals(entry.getParsed(key))) {
for (int i = 0; i < size; i++) {
if (!bytesEqual(key.asciiName(), name(i))) {
continue;
}
values.remove(i);
storeCount--;
@SuppressWarnings("unchecked")
T stored = key.parseBytes(value(i));
if (!value.equals(stored)) {
continue;
}
int writeIdx = i * 2;
int readIdx = (i + 1) * 2;
int readLen = len() - readIdx;
System.arraycopy(namesAndValues, readIdx, namesAndValues, writeIdx, readLen);
size -= 1;
name(size, null);
value(size, null);
return true;
}
return false;
}
/**
* Remove all values for the given key. If there were no values, {@code null} is returned.
*/
/** Remove all values for the given key. If there were no values, {@code null} is returned. */
public <T> Iterable<T> removeAll(Key<T> key) {
List<MetadataEntry> values = store.remove(key.name());
if (values == null) {
if (isEmpty()) {
return null;
}
storeCount -= values.size();
return new ValueIterable<T>(key, values);
int writeIdx = 0;
int readIdx = 0;
List<T> ret = null;
for (; readIdx < size; readIdx++) {
if (bytesEqual(key.asciiName(), name(readIdx))) {
ret = ret != null ? ret : new LinkedList<T>();
ret.add(key.parseBytes(value(readIdx)));
continue;
}
name(writeIdx, name(readIdx));
value(writeIdx, value(readIdx));
writeIdx++;
}
int newSize = writeIdx;
// Multiply by two since namesAndValues is interleaved.
Arrays.fill(namesAndValues, writeIdx * 2, len(), null);
size = newSize;
return ret;
}
/**
@ -283,8 +393,23 @@ public final class Metadata {
*/
@ExperimentalApi
public <T> void discardAll(Key<T> key) {
List<MetadataEntry> removed = store.remove(key.name());
storeCount -= removed != null ? removed.size() : 0;
if (isEmpty()) {
return;
}
int writeIdx = 0;
int readIdx = 0;
for (; readIdx < size; readIdx++) {
if (bytesEqual(key.asciiName(), name(readIdx))) {
continue;
}
name(writeIdx, name(readIdx));
value(writeIdx, value(readIdx));
writeIdx++;
}
int newSize = writeIdx;
// Multiply by two since namesAndValues is interleaved.
Arrays.fill(namesAndValues, writeIdx * 2, len(), null);
size = newSize;
}
/**
@ -302,70 +427,76 @@ public final class Metadata {
*
* <p>This method is intended for transport use only.
*/
@Internal
public byte[][] serialize() {
// 2x for keys + values
byte[][] serialized = new byte[storeCount * 2][];
int i = 0;
for (Map.Entry<String, List<MetadataEntry>> storeEntry : store.entrySet()) {
// Foreach allocates an iterator per.
List<MetadataEntry> values = storeEntry.getValue();
for (int k = 0; k < values.size(); k++) {
serialized[i++] = values.get(k).key != null
? values.get(k).key.asciiName() : storeEntry.getKey().getBytes(US_ASCII);
serialized[i++] = values.get(k).getSerialized();
}
@Nullable
byte[][] serialize() {
if (len() == cap()) {
return namesAndValues;
}
byte[][] serialized = new byte[len()][];
System.arraycopy(namesAndValues, 0, serialized, 0, len());
return serialized;
}
/**
* Perform a simple merge of two sets of metadata.
*/
/** Perform a simple merge of two sets of metadata. */
public void merge(Metadata other) {
Preconditions.checkNotNull(other, "other");
for (Map.Entry<String, List<MetadataEntry>> keyEntry : other.store.entrySet()) {
for (int i = 0; i < keyEntry.getValue().size(); i++) {
// Must copy the MetadataEntries since they are mutated. If the two Metadata objects are
// used from different threads it would cause thread-safety issues.
storeAdd(keyEntry.getKey(), new MetadataEntry(keyEntry.getValue().get(i)));
if (other.isEmpty()) {
return;
}
int remaining = cap() - len();
if (isEmpty() || remaining < other.len()) {
expand(len() + other.len() - remaining);
}
System.arraycopy(other.namesAndValues, 0, namesAndValues, len(), other.len());
size += other.size;
}
/**
* Merge values for the given set of keys into this set of metadata.
*/
/** Merge values for the given set of keys into this set of metadata. */
public void merge(Metadata other, Set<Key<?>> keys) {
Preconditions.checkNotNull(other, "other");
// Use ByteBuffer for equals and hashCode.
Map<ByteBuffer, Key<?>> asciiKeys = new HashMap<ByteBuffer, Key<?>>(keys.size());
for (Key<?> key : keys) {
List<MetadataEntry> values = other.store.get(key.name());
if (values == null) {
continue;
asciiKeys.put(ByteBuffer.wrap(key.asciiName()), key);
}
for (int i = 0; i < values.size(); i++) {
// Must copy the MetadataEntries since they are mutated. If the two Metadata objects are
// used from different threads it would cause thread-safety issues.
storeAdd(key.name(), new MetadataEntry(values.get(i)));
for (int i = 0; i < other.size; i++) {
ByteBuffer wrappedNamed = ByteBuffer.wrap(other.name(i));
if (asciiKeys.containsKey(wrappedNamed)) {
maybeExpand();
name(size, other.name(i));
value(size, other.value(i));
size++;
}
}
}
@Override
public String toString() {
return "Metadata(" + toStringInternal() + ")";
StringBuilder sb = new StringBuilder("Metadata(");
for (int i = 0; i < size; i++) {
if (i != 0) {
sb.append(',');
}
String headerName = new String(name(i), US_ASCII);
sb.append(headerName).append('=');
if (headerName.endsWith(BINARY_HEADER_SUFFIX)) {
sb.append(BaseEncoding.base64().encode(value(i)));
} else {
String headerValue = new String(value(i), US_ASCII);
sb.append(headerValue);
}
}
return sb.append(')').toString();
}
private String toStringInternal() {
return store.toString();
private boolean bytesEqual(byte[] left, byte[] right) {
return Arrays.equals(left, right);
}
/**
* Marshaller for metadata values that are serialized into raw binary.
*/
/** Marshaller for metadata values that are serialized into raw binary. */
public interface BinaryMarshaller<T> {
/**
* Serialize a metadata value to bytes.
*
* @param value to serialize
* @return serialized version of value
*/
@ -373,6 +504,7 @@ public final class Metadata {
/**
* Parse a serialized metadata value from bytes.
*
* @param serialized value of metadata to parse
* @return a parsed instance of type T
*/
@ -382,9 +514,10 @@ public final class Metadata {
/**
* Marshaller for metadata values that are serialized into ASCII strings that contain only
* following characters:
*
* <ul>
* <li>Space: {@code 0x20}, but must not be at the beginning or at the end of the value.
* Leading or trailing whitespace may not be preserved.</li>
* <li>Space: {@code 0x20}, but must not be at the beginning or at the end of the value. Leading
* or trailing whitespace may not be preserved.
* <li>ASCII visible characters ({@code 0x21-0x7E}).
* </ul>
*
@ -404,6 +537,7 @@ public final class Metadata {
/**
* Parse a serialized metadata value from an ASCII string.
*
* @param serialized value of metadata to parse
* @return a parsed instance of type T
*/
@ -416,22 +550,23 @@ public final class Metadata {
* <h3>Valid characters in key names</h3>
*
* <p>Only the following ASCII characters are allowed in the names of keys:
*
* <ul>
* <li>digits: {@code 0-9}</li>
* <li>uppercase letters: {@code A-Z} (normalized to lower)</li>
* <li>lowercase letters: {@code a-z}</li>
* <li>special characters: {@code -_.}</li>
* <li>digits: {@code 0-9}
* <li>uppercase letters: {@code A-Z} (normalized to lower)
* <li>lowercase letters: {@code a-z}
* <li>special characters: {@code -_.}
* </ul>
*
* <p>This is a a strict subset of the HTTP field-name rules. Applications may not send or
* receive metadata with invalid key names. However, the gRPC library may preserve any metadata
* received even if it does not conform to the above limitations. Additionally, if metadata
* contains non conforming field names, they will still be sent. In this way, unknown metadata
* fields are parsed, serialized and preserved, but never interpreted. They are similar to
* protobuf unknown fields.
* <p>This is a a strict subset of the HTTP field-name rules. Applications may not send or receive
* metadata with invalid key names. However, the gRPC library may preserve any metadata received
* even if it does not conform to the above limitations. Additionally, if metadata contains non
* conforming field names, they will still be sent. In this way, unknown metadata fields are
* parsed, serialized and preserved, but never interpreted. They are similar to protobuf unknown
* fields.
*
* <p>Note this has to be the subset of valid HTTP/2 token characters as defined in RFC7230
* Section 3.2.6 and RFC5234 Section B.1</p>
* Section 3.2.6 and RFC5234 Section B.1
*
* @see <a href="https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md">Wire Spec</a>
* @see <a href="https://tools.ietf.org/html/rfc7230#section-3.2.6">RFC7230</a>
@ -496,8 +631,8 @@ public final class Metadata {
continue;
}
checkArgument(VALID_T_CHARS.get(tChar),
"Invalid character '%s' in key name '%s'", tChar, n);
checkArgument(
VALID_T_CHARS.get(tChar), "Invalid character '%s' in key name '%s'", tChar, n);
}
return n;
}
@ -560,6 +695,7 @@ public final class Metadata {
/**
* Serialize a metadata value to bytes.
*
* @param value to serialize
* @return serialized version of value
*/
@ -567,6 +703,7 @@ public final class Metadata {
/**
* Parse a serialized metadata value from bytes.
*
* @param serialized value of metadata to parse
* @return a parsed instance of type T
*/
@ -576,14 +713,14 @@ public final class Metadata {
private static class BinaryKey<T> extends Key<T> {
private final BinaryMarshaller<T> marshaller;
/**
* Keys have a name and a binary marshaller used for serialization.
*/
/** Keys have a name and a binary marshaller used for serialization. */
private BinaryKey(String name, BinaryMarshaller<T> marshaller) {
super(name);
checkArgument(name.endsWith(BINARY_HEADER_SUFFIX),
checkArgument(
name.endsWith(BINARY_HEADER_SUFFIX),
"Binary header is named %s. It must end with %s",
name, BINARY_HEADER_SUFFIX);
name,
BINARY_HEADER_SUFFIX);
checkArgument(name.length() > BINARY_HEADER_SUFFIX.length(), "empty key name");
this.marshaller = checkNotNull(marshaller, "marshaller is null");
}
@ -602,15 +739,14 @@ public final class Metadata {
private static class AsciiKey<T> extends Key<T> {
private final AsciiMarshaller<T> marshaller;
/**
* Keys have a name and an ASCII marshaller used for serialization.
*/
/** Keys have a name and an ASCII marshaller used for serialization. */
private AsciiKey(String name, AsciiMarshaller<T> 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);
name,
BINARY_HEADER_SUFFIX);
this.marshaller = Preconditions.checkNotNull(marshaller, "marshaller");
}
@ -628,15 +764,14 @@ public final class Metadata {
private static final class TrustedAsciiKey<T> extends Key<T> {
private final TrustedAsciiMarshaller<T> marshaller;
/**
* Keys have a name and an ASCII marshaller used for serialization.
*/
/** Keys have a name and an ASCII marshaller used for serialization. */
private TrustedAsciiKey(String name, TrustedAsciiMarshaller<T> 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);
name,
BINARY_HEADER_SUFFIX);
this.marshaller = Preconditions.checkNotNull(marshaller, "marshaller");
}
@ -650,112 +785,4 @@ public final class Metadata {
return marshaller.parseAsciiString(serialized);
}
}
private static class MetadataEntry {
Object parsed;
@SuppressWarnings("rawtypes")
Key key;
boolean isBinary;
byte[] serializedBinary;
/**
* Constructor used when application layer adds a parsed value.
*/
private MetadataEntry(Key<?> key, Object parsed) {
this.parsed = Preconditions.checkNotNull(parsed, "parsed");
this.key = Preconditions.checkNotNull(key, "key");
this.isBinary = key instanceof BinaryKey;
}
/**
* Constructor used when reading a value from the transport.
*/
private MetadataEntry(boolean isBinary, byte[] serialized) {
Preconditions.checkNotNull(serialized, "serialized");
this.serializedBinary = serialized;
this.isBinary = isBinary;
}
/**
* Copy constructor.
*/
private MetadataEntry(MetadataEntry entry) {
this.parsed = entry.parsed;
this.key = entry.key;
this.isBinary = entry.isBinary;
this.serializedBinary = entry.serializedBinary;
}
@SuppressWarnings("unchecked")
public <T> T getParsed(Key<T> key) {
T value = (T) parsed;
if (value != null) {
if (this.key != key) {
// Keys don't match so serialize using the old key
serializedBinary = this.key.toBytes(value);
} else {
return value;
}
}
this.key = key;
if (serializedBinary != null) {
value = key.parseBytes(serializedBinary);
}
parsed = value;
return value;
}
@SuppressWarnings("unchecked")
public byte[] getSerialized() {
return serializedBinary =
serializedBinary == null
? key.toBytes(parsed) : serializedBinary;
}
@Override
public String toString() {
if (!isBinary) {
return new String(getSerialized(), US_ASCII);
} else {
// Assume that the toString of an Object is better than a binary encoding.
if (parsed != null) {
return "" + parsed;
} else {
return Arrays.toString(serializedBinary);
}
}
}
}
private static class ValueIterable<T> implements Iterable<T> {
private final Key<T> key;
private final Iterable<MetadataEntry> entries;
public ValueIterable(Key<T> key, Iterable<MetadataEntry> entries) {
this.key = key;
this.entries = entries;
}
@Override
public Iterator<T> iterator() {
final Iterator<MetadataEntry> iterator = entries.iterator();
class ValueIterator implements Iterator<T> {
@Override public boolean hasNext() {
return iterator.hasNext();
}
@Override public T next() {
return iterator.next().getParsed(key);
}
@Override public void remove() {
// Not implemented to not need to conditionally update {@link #storeCount}.
throw new UnsupportedOperationException();
}
}
return new ValueIterator();
}
}
}

View File

@ -35,6 +35,7 @@ import static com.google.common.base.Charsets.US_ASCII;
import com.google.common.io.BaseEncoding;
import io.grpc.InternalMetadata;
import io.grpc.Metadata;
import java.util.Arrays;
@ -61,7 +62,11 @@ public final class TransportFrameUtil {
* @return the interleaved keys and values.
*/
public static byte[][] toHttp2Headers(Metadata headers) {
byte[][] serializedHeaders = headers.serialize();
byte[][] serializedHeaders = InternalMetadata.serialize(headers);
// TODO(carl-mastrangelo): eventually remove this once all callers are updated.
if (serializedHeaders == null) {
return new byte[][]{};
}
int k = 0;
for (int i = 0; i < serializedHeaders.length; i += 2) {
byte[] key = serializedHeaders[i];

View File

@ -38,7 +38,6 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@ -125,13 +124,21 @@ public class MetadataTest {
assertEquals(null, metadata.get(KEY));
}
@Test
public void discardAll_empty() {
Metadata metadata = new Metadata();
metadata.discardAll(KEY);
assertEquals(null, metadata.getAll(KEY));
assertEquals(null, metadata.get(KEY));
}
@Test
public void testGetAllNoRemove() {
Fish lance = new Fish(LANCE);
Metadata metadata = new Metadata();
metadata.put(KEY, lance);
Iterator<Fish> i = metadata.getAll(KEY).iterator();
assertSame(lance, i.next());
assertEquals(lance, i.next());
thrown.expect(UnsupportedOperationException.class);
i.remove();
@ -142,20 +149,18 @@ public class MetadataTest {
Fish lance = new Fish(LANCE);
Metadata metadata = new Metadata();
metadata.put(KEY, lance);
// Should be able to read same instance out
assertSame(lance, metadata.get(KEY));
assertEquals(lance, metadata.get(KEY));
Iterator<Fish> fishes = metadata.<Fish>getAll(KEY).iterator();
assertTrue(fishes.hasNext());
assertSame(fishes.next(), lance);
assertEquals(fishes.next(), lance);
assertFalse(fishes.hasNext());
byte[][] serialized = metadata.serialize();
assertEquals(2, serialized.length);
assertEquals(new String(serialized[0], US_ASCII), "test-bin");
assertArrayEquals(LANCE_BYTES, serialized[1]);
assertSame(lance, metadata.get(KEY));
// Serialized instance should be cached too
assertSame(serialized[0], metadata.serialize()[0]);
assertSame(serialized[1], metadata.serialize()[1]);
assertEquals(lance, metadata.get(KEY));
assertEquals(serialized[0], metadata.serialize()[0]);
assertEquals(serialized[1], metadata.serialize()[1]);
}
@Test
@ -164,7 +169,7 @@ public class MetadataTest {
Fish lance = raw.get(KEY);
assertEquals(lance, new Fish(LANCE));
// Reading again should return the same parsed instance
assertSame(lance, raw.get(KEY));
assertEquals(lance, raw.get(KEY));
}
@Test
@ -199,7 +204,7 @@ public class MetadataTest {
Iterator<Fish> fishes = h1.<Fish>getAll(KEY).iterator();
assertTrue(fishes.hasNext());
assertSame(fishes.next(), lance);
assertEquals(fishes.next(), lance);
assertFalse(fishes.hasNext());
}
@ -242,15 +247,27 @@ public class MetadataTest {
Metadata h = new Metadata();
h.put(KEY, new Fish("binary"));
h.put(Metadata.Key.of("test", Metadata.ASCII_STRING_MARSHALLER), "ascii");
assertEquals("Metadata({test-bin=[Fish(binary)], test=[ascii]})", h.toString());
assertEquals("Metadata(test-bin=YmluYXJ5,test=ascii)", h.toString());
Metadata t = new Metadata();
t.put(Metadata.Key.of("test", Metadata.ASCII_STRING_MARSHALLER), "ascii");
assertEquals("Metadata({test=[ascii]})", t.toString());
assertEquals("Metadata(test=ascii)", t.toString());
t = new Metadata("test".getBytes(US_ASCII), "ascii".getBytes(US_ASCII),
"test-bin".getBytes(US_ASCII), "binary".getBytes(US_ASCII));
assertEquals("Metadata({test=[ascii], test-bin=[[98, 105, 110, 97, 114, 121]]})", t.toString());
assertEquals("Metadata(test=ascii,test-bin=YmluYXJ5)", t.toString());
}
@Test
public void verifyToString_usingBinary() {
Metadata h = new Metadata();
h.put(KEY, new Fish("binary"));
h.put(Metadata.Key.of("test", Metadata.ASCII_STRING_MARSHALLER), "ascii");
assertEquals("Metadata(test-bin=YmluYXJ5,test=ascii)", h.toString());
Metadata t = new Metadata();
t.put(Metadata.Key.of("test", Metadata.ASCII_STRING_MARSHALLER), "ascii");
assertEquals("Metadata(test=ascii)", t.toString());
}
@Test

View File

@ -41,6 +41,7 @@ import static org.junit.Assert.fail;
import com.google.common.io.BaseEncoding;
import io.grpc.InternalMetadata;
import io.grpc.Metadata;
import io.grpc.Metadata.BinaryMarshaller;
import io.grpc.Metadata.Key;
@ -110,7 +111,7 @@ public class TransportFrameUtilTest {
headers.put(BINARY_STRING_WITHOUT_SUFFIX, NONCOMPLIANT_ASCII_STRING);
byte[][] http2Headers = TransportFrameUtil.toHttp2Headers(headers);
byte[][] rawSerialized = TransportFrameUtil.toRawSerializedHeaders(http2Headers);
Metadata recoveredHeaders = new Metadata(rawSerialized);
Metadata recoveredHeaders = InternalMetadata.newMetadata(rawSerialized);
assertEquals(COMPLIANT_ASCII_STRING, recoveredHeaders.get(PLAIN_STRING));
assertEquals(NONCOMPLIANT_ASCII_STRING, recoveredHeaders.get(BINARY_STRING));
assertNull(recoveredHeaders.get(BINARY_STRING_WITHOUT_SUFFIX));

View File

@ -40,6 +40,7 @@ import static io.netty.util.CharsetUtil.UTF_8;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import io.grpc.InternalMetadata;
import io.grpc.Metadata;
import io.grpc.Status;
import io.grpc.internal.GrpcUtil;
@ -91,9 +92,10 @@ class Utils {
public static Metadata convertHeaders(Http2Headers http2Headers) {
if (http2Headers instanceof GrpcHttp2InboundHeaders) {
return new Metadata(((GrpcHttp2InboundHeaders) http2Headers).namesAndValues());
GrpcHttp2InboundHeaders h = (GrpcHttp2InboundHeaders) http2Headers;
return InternalMetadata.newMetadata(h.numHeaders(), h.namesAndValues());
}
return new Metadata(convertHeadersToArray(http2Headers));
return InternalMetadata.newMetadata(convertHeadersToArray(http2Headers));
}
private static byte[][] convertHeadersToArray(Http2Headers http2Headers) {
@ -141,9 +143,10 @@ class Utils {
public static Metadata convertTrailers(Http2Headers http2Headers) {
if (http2Headers instanceof GrpcHttp2InboundHeaders) {
return new Metadata(((GrpcHttp2InboundHeaders) http2Headers).namesAndValues());
GrpcHttp2InboundHeaders h = (GrpcHttp2InboundHeaders) http2Headers;
return InternalMetadata.newMetadata(h.numHeaders(), h.namesAndValues());
}
return new Metadata(convertHeadersToArray(http2Headers));
return InternalMetadata.newMetadata(convertHeadersToArray(http2Headers));
}
public static Http2Headers convertTrailers(Metadata trailers, boolean headersSent) {

View File

@ -33,6 +33,7 @@ package io.grpc.okhttp;
import com.google.common.base.Preconditions;
import io.grpc.InternalMetadata;
import io.grpc.Metadata;
import io.grpc.internal.TransportFrameUtil;
import io.grpc.okhttp.internal.CipherSuite;
@ -49,11 +50,11 @@ class Utils {
static final int CONNECTION_STREAM_ID = 0;
public static Metadata convertHeaders(List<Header> http2Headers) {
return new Metadata(convertHeadersToArray(http2Headers));
return InternalMetadata.newMetadata(convertHeadersToArray(http2Headers));
}
public static Metadata convertTrailers(List<Header> http2Headers) {
return new Metadata(convertHeadersToArray(http2Headers));
return InternalMetadata.newMetadata(convertHeadersToArray(http2Headers));
}
private static byte[][] convertHeadersToArray(List<Header> http2Headers) {

View File

@ -572,14 +572,17 @@ public abstract class AbstractTransportTest {
clientHeaders.put(asciiKey, "dupvalue");
clientHeaders.put(asciiKey, "dupvalue");
clientHeaders.put(binaryKey, "äbinaryclient");
Metadata clientHeadersCopy = new Metadata();
clientHeadersCopy.merge(clientHeaders);
ClientStream clientStream = client.newStream(methodDescriptor, clientHeaders);
clientStream.start(mockClientStreamListener);
StreamCreation serverStreamCreation
= serverTransportListener.takeStreamOrFail(TIMEOUT_MS, TimeUnit.MILLISECONDS);
assertEquals(methodDescriptor.getFullMethodName(), serverStreamCreation.method);
assertEquals(Lists.newArrayList(clientHeaders.getAll(asciiKey)),
assertEquals(Lists.newArrayList(clientHeadersCopy.getAll(asciiKey)),
Lists.newArrayList(serverStreamCreation.headers.getAll(asciiKey)));
assertEquals(Lists.newArrayList(clientHeaders.getAll(binaryKey)),
assertEquals(Lists.newArrayList(clientHeadersCopy.getAll(binaryKey)),
Lists.newArrayList(serverStreamCreation.headers.getAll(binaryKey)));
ServerStream serverStream = serverStreamCreation.stream;
ServerStreamListener mockServerStreamListener = serverStreamCreation.listener;
@ -605,11 +608,13 @@ public abstract class AbstractTransportTest {
serverHeaders.put(asciiKey, "dupvalue");
serverHeaders.put(asciiKey, "dupvalue");
serverHeaders.put(binaryKey, "äbinaryserver");
Metadata serverHeadersCopy = new Metadata();
serverHeadersCopy.merge(serverHeaders);
serverStream.writeHeaders(serverHeaders);
verify(mockClientStreamListener, timeout(TIMEOUT_MS)).headersRead(metadataCaptor.capture());
assertEquals(Lists.newArrayList(serverHeaders.getAll(asciiKey)),
assertEquals(Lists.newArrayList(serverHeadersCopy.getAll(asciiKey)),
Lists.newArrayList(metadataCaptor.getValue().getAll(asciiKey)));
assertEquals(Lists.newArrayList(serverHeaders.getAll(binaryKey)),
assertEquals(Lists.newArrayList(serverHeadersCopy.getAll(binaryKey)),
Lists.newArrayList(metadataCaptor.getValue().getAll(binaryKey)));
clientStream.request(1);