mirror of https://github.com/grpc/grpc-java.git
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:
parent
16c07ba434
commit
141eed5ed0
|
|
@ -71,10 +71,27 @@ public final class InternalMetadata {
|
||||||
/**
|
/**
|
||||||
* Copy of StandardCharsets, which is only available on Java 1.7 and above.
|
* Copy of StandardCharsets, which is only available on Java 1.7 and above.
|
||||||
*/
|
*/
|
||||||
|
@Internal
|
||||||
public static final Charset US_ASCII = Charset.forName("US-ASCII");
|
public static final Charset US_ASCII = Charset.forName("US-ASCII");
|
||||||
|
|
||||||
@Internal
|
@Internal
|
||||||
public static <T> Key<T> keyOf(String name, TrustedAsciiMarshaller<T> marshaller) {
|
public static <T> Key<T> keyOf(String name, TrustedAsciiMarshaller<T> marshaller) {
|
||||||
return Metadata.Key.of(name, 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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,28 +37,32 @@ import static com.google.common.base.Preconditions.checkNotNull;
|
||||||
|
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
|
import com.google.common.io.BaseEncoding;
|
||||||
|
|
||||||
import io.grpc.InternalMetadata.TrustedAsciiMarshaller;
|
import io.grpc.InternalMetadata.TrustedAsciiMarshaller;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.BitSet;
|
import java.util.BitSet;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
import javax.annotation.concurrent.NotThreadSafe;
|
import javax.annotation.concurrent.NotThreadSafe;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides access to read and write metadata values to be exchanged during a call.
|
* 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
|
* <p>This class is not thread safe, implementations should ensure that header reads and writes do
|
||||||
* do not occur in multiple threads concurrently.
|
* not occur in multiple threads concurrently.
|
||||||
* </p>
|
|
||||||
*/
|
*/
|
||||||
@NotThreadSafe
|
@NotThreadSafe
|
||||||
public final class Metadata {
|
public final class Metadata {
|
||||||
|
|
@ -75,25 +79,25 @@ public final class Metadata {
|
||||||
*
|
*
|
||||||
* <p>This should be used when raw bytes are favored over un-serialized version of object. Can be
|
* <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
|
* 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
|
* <p>Both {@link BinaryMarshaller#toBytes} and {@link BinaryMarshaller#parseBytes} methods do not
|
||||||
* not return a copy of the byte array. Do _not_ modify the byte arrays of either the arguments
|
* return a copy of the byte array. Do _not_ modify the byte arrays of either the arguments or
|
||||||
* or return values.</p>
|
* return values.
|
||||||
*/
|
*/
|
||||||
public static final BinaryMarshaller<byte[]> BINARY_BYTE_MARSHALLER =
|
public static final BinaryMarshaller<byte[]> BINARY_BYTE_MARSHALLER =
|
||||||
new BinaryMarshaller<byte[]>() {
|
new BinaryMarshaller<byte[]>() {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public byte[] toBytes(byte[] value) {
|
public byte[] toBytes(byte[] value) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public byte[] parseBytes(byte[] serialized) {
|
public byte[] parseBytes(byte[] serialized) {
|
||||||
return serialized;
|
return serialized;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simple metadata marshaller that encodes strings as is.
|
* Simple metadata marshaller that encodes strings as is.
|
||||||
|
|
@ -105,114 +109,175 @@ public final class Metadata {
|
||||||
public static final AsciiMarshaller<String> ASCII_STRING_MARSHALLER =
|
public static final AsciiMarshaller<String> ASCII_STRING_MARSHALLER =
|
||||||
new AsciiMarshaller<String>() {
|
new AsciiMarshaller<String>() {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toAsciiString(String value) {
|
public String toAsciiString(String value) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String parseAsciiString(String serialized) {
|
public String parseAsciiString(String serialized) {
|
||||||
return serialized;
|
return serialized;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 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) {
|
||||||
|
return value.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Integer parseAsciiString(String serialized) {
|
||||||
|
return Integer.parseInt(serialized);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simple metadata marshaller that encodes an integer as a signed decimal string.
|
* Constructor called by the transport layer when it receives binary metadata. Metadata will
|
||||||
|
* mutate the passed in array.
|
||||||
*/
|
*/
|
||||||
static final AsciiMarshaller<Integer> INTEGER_MARSHALLER = new AsciiMarshaller<Integer>() {
|
Metadata(byte[]... binaryValues) {
|
||||||
|
this(binaryValues.length / 2, binaryValues);
|
||||||
@Override
|
|
||||||
public String toAsciiString(Integer value) {
|
|
||||||
return value.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Integer parseAsciiString(String serialized) {
|
|
||||||
return Integer.parseInt(serialized);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/** 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.
|
|
||||||
*/
|
|
||||||
// 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]));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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() {}
|
public Metadata() {}
|
||||||
|
|
||||||
private void storeAdd(String name, MetadataEntry value) {
|
/** Returns the total number of key-value headers in this metadata, including duplicates. */
|
||||||
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.
|
|
||||||
*/
|
|
||||||
@Internal
|
@Internal
|
||||||
public int headerCount() {
|
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) {
|
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.
|
* Returns the last metadata entry added with the name 'name' parsed as T.
|
||||||
|
*
|
||||||
* @return the parsed metadata entry or null if there are none.
|
* @return the parsed metadata entry or null if there are none.
|
||||||
*/
|
*/
|
||||||
public <T> T get(Key<T> key) {
|
public <T> T get(Key<T> key) {
|
||||||
List<MetadataEntry> values = store.get(key.name());
|
for (int i = size - 1; i >= 0; i--) {
|
||||||
if (values == null) {
|
if (bytesEqual(key.asciiName(), name(i))) {
|
||||||
return null;
|
return key.parseBytes(value(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
MetadataEntry metadataEntry = values.get(values.size() - 1);
|
|
||||||
return metadataEntry.getParsed(key);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns all the metadata entries named 'name', in the order they were received,
|
* Returns all the metadata entries named 'name', in the order they were received, parsed as T or
|
||||||
* parsed as T or null if there are none. The iterator is not guaranteed to be "live." It may or
|
* null if there are none. The iterator is not guaranteed to be "live." It may or may not be
|
||||||
* may not be accurate if Metadata is mutated.
|
* accurate if Metadata is mutated.
|
||||||
*/
|
*/
|
||||||
public <T> Iterable<T> getAll(Key<T> key) {
|
public <T> Iterable<T> getAll(final Key<T> key) {
|
||||||
if (containsKey(key)) {
|
for (int i = 0; i < size; i++) {
|
||||||
/* This is unmodifiable currently, but could be made to support remove() in the future. If
|
if (bytesEqual(key.asciiName(), name(i))) {
|
||||||
* removal support is added, the {@link #storeCount} variable needs to be updated
|
return new IterableAt<T>(key, i);
|
||||||
* appropriately. */
|
}
|
||||||
return new ValueIterable<T>(key, store.get(key.name()));
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -222,8 +287,17 @@ public final class Metadata {
|
||||||
*
|
*
|
||||||
* @return unmodifiable Set of keys
|
* @return unmodifiable Set of keys
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings("deprecation") // The String ctor is deprecated, but fast.
|
||||||
public Set<String> keys() {
|
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) {
|
public <T> void put(Key<T> key, T value) {
|
||||||
Preconditions.checkNotNull(key, "key");
|
Preconditions.checkNotNull(key, "key");
|
||||||
Preconditions.checkNotNull(value, "value");
|
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,42 +341,75 @@ public final class Metadata {
|
||||||
public <T> boolean remove(Key<T> key, T value) {
|
public <T> boolean remove(Key<T> key, T value) {
|
||||||
Preconditions.checkNotNull(key, "key");
|
Preconditions.checkNotNull(key, "key");
|
||||||
Preconditions.checkNotNull(value, "value");
|
Preconditions.checkNotNull(value, "value");
|
||||||
List<MetadataEntry> values = store.get(key.name());
|
for (int i = 0; i < size; i++) {
|
||||||
if (values == null) {
|
if (!bytesEqual(key.asciiName(), name(i))) {
|
||||||
return false;
|
|
||||||
}
|
|
||||||
for (int i = 0; i < values.size(); i++) {
|
|
||||||
MetadataEntry entry = values.get(i);
|
|
||||||
if (!value.equals(entry.getParsed(key))) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
values.remove(i);
|
@SuppressWarnings("unchecked")
|
||||||
storeCount--;
|
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 true;
|
||||||
}
|
}
|
||||||
return false;
|
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) {
|
public <T> Iterable<T> removeAll(Key<T> key) {
|
||||||
List<MetadataEntry> values = store.remove(key.name());
|
if (isEmpty()) {
|
||||||
if (values == null) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
storeCount -= values.size();
|
int writeIdx = 0;
|
||||||
return new ValueIterable<T>(key, values);
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove all values for the given key without returning them. This is a minor performance
|
* Remove all values for the given key without returning them. This is a minor performance
|
||||||
* optimization if you do not need the previous values.
|
* optimization if you do not need the previous values.
|
||||||
*/
|
*/
|
||||||
@ExperimentalApi
|
@ExperimentalApi
|
||||||
public <T> void discardAll(Key<T> key) {
|
public <T> void discardAll(Key<T> key) {
|
||||||
List<MetadataEntry> removed = store.remove(key.name());
|
if (isEmpty()) {
|
||||||
storeCount -= removed != null ? removed.size() : 0;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -294,78 +419,84 @@ public final class Metadata {
|
||||||
* result[i*2+1] are values.
|
* result[i*2+1] are values.
|
||||||
*
|
*
|
||||||
* <p>Names are ASCII string bytes that contains only the characters listed in the class comment
|
* <p>Names are ASCII string bytes that contains only the characters listed in the class comment
|
||||||
* of {@link Key}. If the name ends with {@code "-bin"}, the value can be raw binary. Otherwise,
|
* of {@link Key}. If the name ends with {@code "-bin"}, the value can be raw binary. Otherwise,
|
||||||
* the value must contain only characters listed in the class comments of {@link AsciiMarshaller}
|
* the value must contain only characters listed in the class comments of {@link AsciiMarshaller}
|
||||||
*
|
*
|
||||||
* <p>The returned individual byte arrays <em>must not</em> be modified. However, the top level
|
* <p>The returned individual byte arrays <em>must not</em> be modified. However, the top level
|
||||||
* array may be modified.
|
* array may be modified.
|
||||||
*
|
*
|
||||||
* <p>This method is intended for transport use only.
|
* <p>This method is intended for transport use only.
|
||||||
*/
|
*/
|
||||||
@Internal
|
@Nullable
|
||||||
public byte[][] serialize() {
|
byte[][] serialize() {
|
||||||
// 2x for keys + values
|
if (len() == cap()) {
|
||||||
byte[][] serialized = new byte[storeCount * 2][];
|
return namesAndValues;
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
byte[][] serialized = new byte[len()][];
|
||||||
|
System.arraycopy(namesAndValues, 0, serialized, 0, len());
|
||||||
return serialized;
|
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) {
|
public void merge(Metadata other) {
|
||||||
Preconditions.checkNotNull(other, "other");
|
if (other.isEmpty()) {
|
||||||
for (Map.Entry<String, List<MetadataEntry>> keyEntry : other.store.entrySet()) {
|
return;
|
||||||
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)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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) {
|
public void merge(Metadata other, Set<Key<?>> keys) {
|
||||||
Preconditions.checkNotNull(other, "other");
|
Preconditions.checkNotNull(other, "other");
|
||||||
|
// Use ByteBuffer for equals and hashCode.
|
||||||
|
Map<ByteBuffer, Key<?>> asciiKeys = new HashMap<ByteBuffer, Key<?>>(keys.size());
|
||||||
for (Key<?> key : keys) {
|
for (Key<?> key : keys) {
|
||||||
List<MetadataEntry> values = other.store.get(key.name());
|
asciiKeys.put(ByteBuffer.wrap(key.asciiName()), key);
|
||||||
if (values == null) {
|
}
|
||||||
continue;
|
for (int i = 0; i < other.size; i++) {
|
||||||
}
|
ByteBuffer wrappedNamed = ByteBuffer.wrap(other.name(i));
|
||||||
for (int i = 0; i < values.size(); i++) {
|
if (asciiKeys.containsKey(wrappedNamed)) {
|
||||||
// Must copy the MetadataEntries since they are mutated. If the two Metadata objects are
|
maybeExpand();
|
||||||
// used from different threads it would cause thread-safety issues.
|
name(size, other.name(i));
|
||||||
storeAdd(key.name(), new MetadataEntry(values.get(i)));
|
value(size, other.value(i));
|
||||||
|
size++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
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() {
|
private boolean bytesEqual(byte[] left, byte[] right) {
|
||||||
return store.toString();
|
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> {
|
public interface BinaryMarshaller<T> {
|
||||||
/**
|
/**
|
||||||
* Serialize a metadata value to bytes.
|
* Serialize a metadata value to bytes.
|
||||||
|
*
|
||||||
* @param value to serialize
|
* @param value to serialize
|
||||||
* @return serialized version of value
|
* @return serialized version of value
|
||||||
*/
|
*/
|
||||||
|
|
@ -373,6 +504,7 @@ public final class Metadata {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse a serialized metadata value from bytes.
|
* Parse a serialized metadata value from bytes.
|
||||||
|
*
|
||||||
* @param serialized value of metadata to parse
|
* @param serialized value of metadata to parse
|
||||||
* @return a parsed instance of type T
|
* @return a parsed instance of type T
|
||||||
*/
|
*/
|
||||||
|
|
@ -382,10 +514,11 @@ public final class Metadata {
|
||||||
/**
|
/**
|
||||||
* Marshaller for metadata values that are serialized into ASCII strings that contain only
|
* Marshaller for metadata values that are serialized into ASCII strings that contain only
|
||||||
* following characters:
|
* following characters:
|
||||||
|
*
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>Space: {@code 0x20}, but must not be at the beginning or at the end of the value.
|
* <li>Space: {@code 0x20}, but must not be at the beginning or at the end of the value. Leading
|
||||||
* Leading or trailing whitespace may not be preserved.</li>
|
* or trailing whitespace may not be preserved.
|
||||||
* <li>ASCII visible characters ({@code 0x21-0x7E}).
|
* <li>ASCII visible characters ({@code 0x21-0x7E}).
|
||||||
* </ul>
|
* </ul>
|
||||||
*
|
*
|
||||||
* <p>Note this has to be the subset of valid characters in {@code field-content} from RFC 7230
|
* <p>Note this has to be the subset of valid characters in {@code field-content} from RFC 7230
|
||||||
|
|
@ -404,6 +537,7 @@ public final class Metadata {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse a serialized metadata value from an ASCII string.
|
* Parse a serialized metadata value from an ASCII string.
|
||||||
|
*
|
||||||
* @param serialized value of metadata to parse
|
* @param serialized value of metadata to parse
|
||||||
* @return a parsed instance of type T
|
* @return a parsed instance of type T
|
||||||
*/
|
*/
|
||||||
|
|
@ -416,22 +550,23 @@ public final class Metadata {
|
||||||
* <h3>Valid characters in key names</h3>
|
* <h3>Valid characters in key names</h3>
|
||||||
*
|
*
|
||||||
* <p>Only the following ASCII characters are allowed in the names of keys:
|
* <p>Only the following ASCII characters are allowed in the names of keys:
|
||||||
|
*
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>digits: {@code 0-9}</li>
|
* <li>digits: {@code 0-9}
|
||||||
* <li>uppercase letters: {@code A-Z} (normalized to lower)</li>
|
* <li>uppercase letters: {@code A-Z} (normalized to lower)
|
||||||
* <li>lowercase letters: {@code a-z}</li>
|
* <li>lowercase letters: {@code a-z}
|
||||||
* <li>special characters: {@code -_.}</li>
|
* <li>special characters: {@code -_.}
|
||||||
* </ul>
|
* </ul>
|
||||||
*
|
*
|
||||||
* <p>This is a a strict subset of the HTTP field-name rules. Applications may not send or
|
* <p>This is a a strict subset of the HTTP field-name rules. Applications may not send or receive
|
||||||
* receive metadata with invalid key names. However, the gRPC library may preserve any metadata
|
* metadata with invalid key names. However, the gRPC library may preserve any metadata received
|
||||||
* received even if it does not conform to the above limitations. Additionally, if metadata
|
* even if it does not conform to the above limitations. Additionally, if metadata contains non
|
||||||
* contains non conforming field names, they will still be sent. In this way, unknown metadata
|
* conforming field names, they will still be sent. In this way, unknown metadata fields are
|
||||||
* fields are parsed, serialized and preserved, but never interpreted. They are similar to
|
* parsed, serialized and preserved, but never interpreted. They are similar to protobuf unknown
|
||||||
* protobuf unknown fields.
|
* fields.
|
||||||
*
|
*
|
||||||
* <p>Note this has to be the subset of valid HTTP/2 token characters as defined in RFC7230
|
* <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://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>
|
* @see <a href="https://tools.ietf.org/html/rfc7230#section-3.2.6">RFC7230</a>
|
||||||
|
|
@ -446,7 +581,7 @@ public final class Metadata {
|
||||||
* Creates a key for a binary header.
|
* Creates a key for a binary header.
|
||||||
*
|
*
|
||||||
* @param name Must contain only the valid key characters as defined in the class comment. Must
|
* @param name Must contain only the valid key characters as defined in the class comment. Must
|
||||||
* end with {@link #BINARY_HEADER_SUFFIX}.
|
* end with {@link #BINARY_HEADER_SUFFIX}.
|
||||||
*/
|
*/
|
||||||
public static <T> Key<T> of(String name, BinaryMarshaller<T> marshaller) {
|
public static <T> Key<T> of(String name, BinaryMarshaller<T> marshaller) {
|
||||||
return new BinaryKey<T>(name, marshaller);
|
return new BinaryKey<T>(name, marshaller);
|
||||||
|
|
@ -456,7 +591,7 @@ public final class Metadata {
|
||||||
* Creates a key for an ASCII header.
|
* Creates a key for an ASCII header.
|
||||||
*
|
*
|
||||||
* @param name Must contain only the valid key characters as defined in the class comment. Must
|
* @param name Must contain only the valid key characters as defined in the class comment. Must
|
||||||
* <b>not</b> end with {@link #BINARY_HEADER_SUFFIX}
|
* <b>not</b> end with {@link #BINARY_HEADER_SUFFIX}
|
||||||
*/
|
*/
|
||||||
public static <T> Key<T> of(String name, AsciiMarshaller<T> marshaller) {
|
public static <T> Key<T> of(String name, AsciiMarshaller<T> marshaller) {
|
||||||
return new AsciiKey<T>(name, marshaller);
|
return new AsciiKey<T>(name, marshaller);
|
||||||
|
|
@ -472,7 +607,7 @@ public final class Metadata {
|
||||||
private final byte[] nameBytes;
|
private final byte[] nameBytes;
|
||||||
|
|
||||||
private static BitSet generateValidTChars() {
|
private static BitSet generateValidTChars() {
|
||||||
BitSet valid = new BitSet(0x7f);
|
BitSet valid = new BitSet(0x7f);
|
||||||
valid.set('-');
|
valid.set('-');
|
||||||
valid.set('_');
|
valid.set('_');
|
||||||
valid.set('.');
|
valid.set('.');
|
||||||
|
|
@ -496,8 +631,8 @@ public final class Metadata {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
checkArgument(VALID_T_CHARS.get(tChar),
|
checkArgument(
|
||||||
"Invalid character '%s' in key name '%s'", tChar, n);
|
VALID_T_CHARS.get(tChar), "Invalid character '%s' in key name '%s'", tChar, n);
|
||||||
}
|
}
|
||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
|
|
@ -560,6 +695,7 @@ public final class Metadata {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Serialize a metadata value to bytes.
|
* Serialize a metadata value to bytes.
|
||||||
|
*
|
||||||
* @param value to serialize
|
* @param value to serialize
|
||||||
* @return serialized version of value
|
* @return serialized version of value
|
||||||
*/
|
*/
|
||||||
|
|
@ -567,6 +703,7 @@ public final class Metadata {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse a serialized metadata value from bytes.
|
* Parse a serialized metadata value from bytes.
|
||||||
|
*
|
||||||
* @param serialized value of metadata to parse
|
* @param serialized value of metadata to parse
|
||||||
* @return a parsed instance of type T
|
* @return a parsed instance of type T
|
||||||
*/
|
*/
|
||||||
|
|
@ -576,14 +713,14 @@ public final class Metadata {
|
||||||
private static class BinaryKey<T> extends Key<T> {
|
private static class BinaryKey<T> extends Key<T> {
|
||||||
private final BinaryMarshaller<T> marshaller;
|
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) {
|
private BinaryKey(String name, BinaryMarshaller<T> marshaller) {
|
||||||
super(name);
|
super(name);
|
||||||
checkArgument(name.endsWith(BINARY_HEADER_SUFFIX),
|
checkArgument(
|
||||||
|
name.endsWith(BINARY_HEADER_SUFFIX),
|
||||||
"Binary header is named %s. It must end with %s",
|
"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");
|
checkArgument(name.length() > BINARY_HEADER_SUFFIX.length(), "empty key name");
|
||||||
this.marshaller = checkNotNull(marshaller, "marshaller is null");
|
this.marshaller = checkNotNull(marshaller, "marshaller is null");
|
||||||
}
|
}
|
||||||
|
|
@ -602,15 +739,14 @@ public final class Metadata {
|
||||||
private static class AsciiKey<T> extends Key<T> {
|
private static class AsciiKey<T> extends Key<T> {
|
||||||
private final AsciiMarshaller<T> marshaller;
|
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) {
|
private AsciiKey(String name, AsciiMarshaller<T> marshaller) {
|
||||||
super(name);
|
super(name);
|
||||||
Preconditions.checkArgument(
|
Preconditions.checkArgument(
|
||||||
!name.endsWith(BINARY_HEADER_SUFFIX),
|
!name.endsWith(BINARY_HEADER_SUFFIX),
|
||||||
"ASCII header is named %s. Only binary headers may end with %s",
|
"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");
|
this.marshaller = Preconditions.checkNotNull(marshaller, "marshaller");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -628,15 +764,14 @@ public final class Metadata {
|
||||||
private static final class TrustedAsciiKey<T> extends Key<T> {
|
private static final class TrustedAsciiKey<T> extends Key<T> {
|
||||||
private final TrustedAsciiMarshaller<T> marshaller;
|
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) {
|
private TrustedAsciiKey(String name, TrustedAsciiMarshaller<T> marshaller) {
|
||||||
super(name);
|
super(name);
|
||||||
Preconditions.checkArgument(
|
Preconditions.checkArgument(
|
||||||
!name.endsWith(BINARY_HEADER_SUFFIX),
|
!name.endsWith(BINARY_HEADER_SUFFIX),
|
||||||
"ASCII header is named %s. Only binary headers may end with %s",
|
"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");
|
this.marshaller = Preconditions.checkNotNull(marshaller, "marshaller");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -650,112 +785,4 @@ public final class Metadata {
|
||||||
return marshaller.parseAsciiString(serialized);
|
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ import static com.google.common.base.Charsets.US_ASCII;
|
||||||
|
|
||||||
import com.google.common.io.BaseEncoding;
|
import com.google.common.io.BaseEncoding;
|
||||||
|
|
||||||
|
import io.grpc.InternalMetadata;
|
||||||
import io.grpc.Metadata;
|
import io.grpc.Metadata;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
@ -61,7 +62,11 @@ public final class TransportFrameUtil {
|
||||||
* @return the interleaved keys and values.
|
* @return the interleaved keys and values.
|
||||||
*/
|
*/
|
||||||
public static byte[][] toHttp2Headers(Metadata headers) {
|
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;
|
int k = 0;
|
||||||
for (int i = 0; i < serializedHeaders.length; i += 2) {
|
for (int i = 0; i < serializedHeaders.length; i += 2) {
|
||||||
byte[] key = serializedHeaders[i];
|
byte[] key = serializedHeaders[i];
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,6 @@ import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertNotEquals;
|
import static org.junit.Assert.assertNotEquals;
|
||||||
import static org.junit.Assert.assertNull;
|
import static org.junit.Assert.assertNull;
|
||||||
import static org.junit.Assert.assertSame;
|
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
import static org.junit.Assert.fail;
|
import static org.junit.Assert.fail;
|
||||||
|
|
||||||
|
|
@ -125,13 +124,21 @@ public class MetadataTest {
|
||||||
assertEquals(null, metadata.get(KEY));
|
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
|
@Test
|
||||||
public void testGetAllNoRemove() {
|
public void testGetAllNoRemove() {
|
||||||
Fish lance = new Fish(LANCE);
|
Fish lance = new Fish(LANCE);
|
||||||
Metadata metadata = new Metadata();
|
Metadata metadata = new Metadata();
|
||||||
metadata.put(KEY, lance);
|
metadata.put(KEY, lance);
|
||||||
Iterator<Fish> i = metadata.getAll(KEY).iterator();
|
Iterator<Fish> i = metadata.getAll(KEY).iterator();
|
||||||
assertSame(lance, i.next());
|
assertEquals(lance, i.next());
|
||||||
|
|
||||||
thrown.expect(UnsupportedOperationException.class);
|
thrown.expect(UnsupportedOperationException.class);
|
||||||
i.remove();
|
i.remove();
|
||||||
|
|
@ -142,20 +149,18 @@ public class MetadataTest {
|
||||||
Fish lance = new Fish(LANCE);
|
Fish lance = new Fish(LANCE);
|
||||||
Metadata metadata = new Metadata();
|
Metadata metadata = new Metadata();
|
||||||
metadata.put(KEY, lance);
|
metadata.put(KEY, lance);
|
||||||
// Should be able to read same instance out
|
assertEquals(lance, metadata.get(KEY));
|
||||||
assertSame(lance, metadata.get(KEY));
|
|
||||||
Iterator<Fish> fishes = metadata.<Fish>getAll(KEY).iterator();
|
Iterator<Fish> fishes = metadata.<Fish>getAll(KEY).iterator();
|
||||||
assertTrue(fishes.hasNext());
|
assertTrue(fishes.hasNext());
|
||||||
assertSame(fishes.next(), lance);
|
assertEquals(fishes.next(), lance);
|
||||||
assertFalse(fishes.hasNext());
|
assertFalse(fishes.hasNext());
|
||||||
byte[][] serialized = metadata.serialize();
|
byte[][] serialized = metadata.serialize();
|
||||||
assertEquals(2, serialized.length);
|
assertEquals(2, serialized.length);
|
||||||
assertEquals(new String(serialized[0], US_ASCII), "test-bin");
|
assertEquals(new String(serialized[0], US_ASCII), "test-bin");
|
||||||
assertArrayEquals(LANCE_BYTES, serialized[1]);
|
assertArrayEquals(LANCE_BYTES, serialized[1]);
|
||||||
assertSame(lance, metadata.get(KEY));
|
assertEquals(lance, metadata.get(KEY));
|
||||||
// Serialized instance should be cached too
|
assertEquals(serialized[0], metadata.serialize()[0]);
|
||||||
assertSame(serialized[0], metadata.serialize()[0]);
|
assertEquals(serialized[1], metadata.serialize()[1]);
|
||||||
assertSame(serialized[1], metadata.serialize()[1]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -164,7 +169,7 @@ public class MetadataTest {
|
||||||
Fish lance = raw.get(KEY);
|
Fish lance = raw.get(KEY);
|
||||||
assertEquals(lance, new Fish(LANCE));
|
assertEquals(lance, new Fish(LANCE));
|
||||||
// Reading again should return the same parsed instance
|
// Reading again should return the same parsed instance
|
||||||
assertSame(lance, raw.get(KEY));
|
assertEquals(lance, raw.get(KEY));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -199,7 +204,7 @@ public class MetadataTest {
|
||||||
|
|
||||||
Iterator<Fish> fishes = h1.<Fish>getAll(KEY).iterator();
|
Iterator<Fish> fishes = h1.<Fish>getAll(KEY).iterator();
|
||||||
assertTrue(fishes.hasNext());
|
assertTrue(fishes.hasNext());
|
||||||
assertSame(fishes.next(), lance);
|
assertEquals(fishes.next(), lance);
|
||||||
assertFalse(fishes.hasNext());
|
assertFalse(fishes.hasNext());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -242,15 +247,27 @@ public class MetadataTest {
|
||||||
Metadata h = new Metadata();
|
Metadata h = new Metadata();
|
||||||
h.put(KEY, new Fish("binary"));
|
h.put(KEY, new Fish("binary"));
|
||||||
h.put(Metadata.Key.of("test", Metadata.ASCII_STRING_MARSHALLER), "ascii");
|
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();
|
Metadata t = new Metadata();
|
||||||
t.put(Metadata.Key.of("test", Metadata.ASCII_STRING_MARSHALLER), "ascii");
|
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),
|
t = new Metadata("test".getBytes(US_ASCII), "ascii".getBytes(US_ASCII),
|
||||||
"test-bin".getBytes(US_ASCII), "binary".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
|
@Test
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ import static org.junit.Assert.fail;
|
||||||
|
|
||||||
import com.google.common.io.BaseEncoding;
|
import com.google.common.io.BaseEncoding;
|
||||||
|
|
||||||
|
import io.grpc.InternalMetadata;
|
||||||
import io.grpc.Metadata;
|
import io.grpc.Metadata;
|
||||||
import io.grpc.Metadata.BinaryMarshaller;
|
import io.grpc.Metadata.BinaryMarshaller;
|
||||||
import io.grpc.Metadata.Key;
|
import io.grpc.Metadata.Key;
|
||||||
|
|
@ -110,7 +111,7 @@ public class TransportFrameUtilTest {
|
||||||
headers.put(BINARY_STRING_WITHOUT_SUFFIX, NONCOMPLIANT_ASCII_STRING);
|
headers.put(BINARY_STRING_WITHOUT_SUFFIX, NONCOMPLIANT_ASCII_STRING);
|
||||||
byte[][] http2Headers = TransportFrameUtil.toHttp2Headers(headers);
|
byte[][] http2Headers = TransportFrameUtil.toHttp2Headers(headers);
|
||||||
byte[][] rawSerialized = TransportFrameUtil.toRawSerializedHeaders(http2Headers);
|
byte[][] rawSerialized = TransportFrameUtil.toRawSerializedHeaders(http2Headers);
|
||||||
Metadata recoveredHeaders = new Metadata(rawSerialized);
|
Metadata recoveredHeaders = InternalMetadata.newMetadata(rawSerialized);
|
||||||
assertEquals(COMPLIANT_ASCII_STRING, recoveredHeaders.get(PLAIN_STRING));
|
assertEquals(COMPLIANT_ASCII_STRING, recoveredHeaders.get(PLAIN_STRING));
|
||||||
assertEquals(NONCOMPLIANT_ASCII_STRING, recoveredHeaders.get(BINARY_STRING));
|
assertEquals(NONCOMPLIANT_ASCII_STRING, recoveredHeaders.get(BINARY_STRING));
|
||||||
assertNull(recoveredHeaders.get(BINARY_STRING_WITHOUT_SUFFIX));
|
assertNull(recoveredHeaders.get(BINARY_STRING_WITHOUT_SUFFIX));
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ import static io.netty.util.CharsetUtil.UTF_8;
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
|
|
||||||
|
import io.grpc.InternalMetadata;
|
||||||
import io.grpc.Metadata;
|
import io.grpc.Metadata;
|
||||||
import io.grpc.Status;
|
import io.grpc.Status;
|
||||||
import io.grpc.internal.GrpcUtil;
|
import io.grpc.internal.GrpcUtil;
|
||||||
|
|
@ -91,9 +92,10 @@ class Utils {
|
||||||
|
|
||||||
public static Metadata convertHeaders(Http2Headers http2Headers) {
|
public static Metadata convertHeaders(Http2Headers http2Headers) {
|
||||||
if (http2Headers instanceof GrpcHttp2InboundHeaders) {
|
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) {
|
private static byte[][] convertHeadersToArray(Http2Headers http2Headers) {
|
||||||
|
|
@ -141,9 +143,10 @@ class Utils {
|
||||||
|
|
||||||
public static Metadata convertTrailers(Http2Headers http2Headers) {
|
public static Metadata convertTrailers(Http2Headers http2Headers) {
|
||||||
if (http2Headers instanceof GrpcHttp2InboundHeaders) {
|
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) {
|
public static Http2Headers convertTrailers(Metadata trailers, boolean headersSent) {
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ package io.grpc.okhttp;
|
||||||
|
|
||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
|
|
||||||
|
import io.grpc.InternalMetadata;
|
||||||
import io.grpc.Metadata;
|
import io.grpc.Metadata;
|
||||||
import io.grpc.internal.TransportFrameUtil;
|
import io.grpc.internal.TransportFrameUtil;
|
||||||
import io.grpc.okhttp.internal.CipherSuite;
|
import io.grpc.okhttp.internal.CipherSuite;
|
||||||
|
|
@ -49,11 +50,11 @@ class Utils {
|
||||||
static final int CONNECTION_STREAM_ID = 0;
|
static final int CONNECTION_STREAM_ID = 0;
|
||||||
|
|
||||||
public static Metadata convertHeaders(List<Header> http2Headers) {
|
public static Metadata convertHeaders(List<Header> http2Headers) {
|
||||||
return new Metadata(convertHeadersToArray(http2Headers));
|
return InternalMetadata.newMetadata(convertHeadersToArray(http2Headers));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Metadata convertTrailers(List<Header> 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) {
|
private static byte[][] convertHeadersToArray(List<Header> http2Headers) {
|
||||||
|
|
|
||||||
|
|
@ -572,14 +572,17 @@ public abstract class AbstractTransportTest {
|
||||||
clientHeaders.put(asciiKey, "dupvalue");
|
clientHeaders.put(asciiKey, "dupvalue");
|
||||||
clientHeaders.put(asciiKey, "dupvalue");
|
clientHeaders.put(asciiKey, "dupvalue");
|
||||||
clientHeaders.put(binaryKey, "äbinaryclient");
|
clientHeaders.put(binaryKey, "äbinaryclient");
|
||||||
|
Metadata clientHeadersCopy = new Metadata();
|
||||||
|
|
||||||
|
clientHeadersCopy.merge(clientHeaders);
|
||||||
ClientStream clientStream = client.newStream(methodDescriptor, clientHeaders);
|
ClientStream clientStream = client.newStream(methodDescriptor, clientHeaders);
|
||||||
clientStream.start(mockClientStreamListener);
|
clientStream.start(mockClientStreamListener);
|
||||||
StreamCreation serverStreamCreation
|
StreamCreation serverStreamCreation
|
||||||
= serverTransportListener.takeStreamOrFail(TIMEOUT_MS, TimeUnit.MILLISECONDS);
|
= serverTransportListener.takeStreamOrFail(TIMEOUT_MS, TimeUnit.MILLISECONDS);
|
||||||
assertEquals(methodDescriptor.getFullMethodName(), serverStreamCreation.method);
|
assertEquals(methodDescriptor.getFullMethodName(), serverStreamCreation.method);
|
||||||
assertEquals(Lists.newArrayList(clientHeaders.getAll(asciiKey)),
|
assertEquals(Lists.newArrayList(clientHeadersCopy.getAll(asciiKey)),
|
||||||
Lists.newArrayList(serverStreamCreation.headers.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)));
|
Lists.newArrayList(serverStreamCreation.headers.getAll(binaryKey)));
|
||||||
ServerStream serverStream = serverStreamCreation.stream;
|
ServerStream serverStream = serverStreamCreation.stream;
|
||||||
ServerStreamListener mockServerStreamListener = serverStreamCreation.listener;
|
ServerStreamListener mockServerStreamListener = serverStreamCreation.listener;
|
||||||
|
|
@ -605,11 +608,13 @@ public abstract class AbstractTransportTest {
|
||||||
serverHeaders.put(asciiKey, "dupvalue");
|
serverHeaders.put(asciiKey, "dupvalue");
|
||||||
serverHeaders.put(asciiKey, "dupvalue");
|
serverHeaders.put(asciiKey, "dupvalue");
|
||||||
serverHeaders.put(binaryKey, "äbinaryserver");
|
serverHeaders.put(binaryKey, "äbinaryserver");
|
||||||
|
Metadata serverHeadersCopy = new Metadata();
|
||||||
|
serverHeadersCopy.merge(serverHeaders);
|
||||||
serverStream.writeHeaders(serverHeaders);
|
serverStream.writeHeaders(serverHeaders);
|
||||||
verify(mockClientStreamListener, timeout(TIMEOUT_MS)).headersRead(metadataCaptor.capture());
|
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)));
|
Lists.newArrayList(metadataCaptor.getValue().getAll(asciiKey)));
|
||||||
assertEquals(Lists.newArrayList(serverHeaders.getAll(binaryKey)),
|
assertEquals(Lists.newArrayList(serverHeadersCopy.getAll(binaryKey)),
|
||||||
Lists.newArrayList(metadataCaptor.getValue().getAll(binaryKey)));
|
Lists.newArrayList(metadataCaptor.getValue().getAll(binaryKey)));
|
||||||
|
|
||||||
clientStream.request(1);
|
clientStream.request(1);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue