Support for lazily serialized values in Metadata.

First add a new a Metadata.BinaryStreamMarshaller interface which
serializes to/from instances of InputStream, and a corresponding
Key.of() factory method.

Values set with this type of key will be kept unserialized internally,
alongside a reference to the Marshaller. A new method
InternalMetadata.serializePartial(), returns values which are either
byte[] or InputStream, and allows transport-specific handling of
lazily-serialized values.

For the regular serialize() method, stream-marshalled values will be
converted to byte[] via an InputStreams.
This commit is contained in:
markb74 2019-12-12 23:34:30 +00:00 committed by Eric Anderson
parent 75b9fee2d1
commit d107859145
3 changed files with 440 additions and 25 deletions

View File

@ -18,6 +18,7 @@ package io.grpc;
import com.google.common.io.BaseEncoding;
import io.grpc.Metadata.AsciiMarshaller;
import io.grpc.Metadata.BinaryStreamMarshaller;
import io.grpc.Metadata.Key;
import java.nio.charset.Charset;
@ -82,4 +83,46 @@ public final class InternalMetadata {
public static int headerCount(Metadata md) {
return md.headerCount();
}
/**
* Serializes all metadata entries, leaving some values as {@link InputStream}s.
*
* <p>Produces serialized names and values interleaved. result[i*2] are names, while
* result[i*2+1] are values.
*
* <p>Names are byte arrays as described according to the {@link Metadata#serialize}
* method. Values are either byte arrays or {@link InputStream}s.
*/
@Internal
public static Object[] serializePartial(Metadata md) {
return md.serializePartial();
}
/**
* Creates a holder for a pre-parsed value read by the transport.
*
* @param marshaller The {@link Metadata#BinaryStreamMarshaller} associated with this value.
* @param value The value to store.
* @return an object holding the pre-parsed value for this key.
*/
@Internal
public static <T> Object parsedValue(BinaryStreamMarshaller<T> marshaller, T value) {
return new Metadata.LazyValue<>(marshaller, value);
}
/**
* Creates a new {@link Metadata} instance from serialized data,
* with some values pre-parsed. Metadata will mutate the passed in array.
*
* @param usedNames The number of names used.
* @param namesAndValues An array of interleaved names and values,
* with each name (at even indices) represented as a byte array,
* and each value (at odd indices) represented as either a byte
* array or an object returned by the {@link #parsedValue}
* method.
*/
@Internal
public static Metadata newMetadataWithParsedValues(int usedNames, Object[] namesAndValues) {
return new Metadata(usedNames, namesAndValues);
}
}

View File

@ -23,6 +23,10 @@ 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 com.google.common.io.ByteStreams;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
@ -118,27 +122,41 @@ public final class Metadata {
* Constructor called by the transport layer when it receives binary metadata. Metadata will
* mutate the passed in array.
*
* @param usedNames the number of
* @param usedNames the number of names
*/
Metadata(int usedNames, byte[]... binaryValues) {
assert (binaryValues.length & 1) == 0 : "Odd number of key-value pairs " + binaryValues.length;
size = usedNames;
namesAndValues = binaryValues;
this(usedNames, (Object[]) binaryValues);
}
private byte[][] namesAndValues;
/**
* Constructor called by the transport layer when it receives partially-parsed metadata.
* Metadata will mutate the passed in array.
*
* @param usedNames the number of names
* @param namesAndValues an array of interleaved names and values, with each name
* (at even indices) represented by a byte array, and values (at odd indices) as
* described by {@link InternalMetadata#newMetadataWithParsedValues}.
*/
Metadata(int usedNames, Object[] namesAndValues) {
assert (namesAndValues.length & 1) == 0
: "Odd number of key-value pairs " + namesAndValues.length;
size = usedNames;
this.namesAndValues = namesAndValues;
}
private Object[] namesAndValues;
// The unscaled number of headers present.
private int size;
private byte[] name(int i) {
return namesAndValues[i * 2];
return (byte[]) namesAndValues[i * 2];
}
private void name(int i, byte[] name) {
namesAndValues[i * 2] = name;
}
private byte[] value(int i) {
private Object value(int i) {
return namesAndValues[i * 2 + 1];
}
@ -146,6 +164,41 @@ public final class Metadata {
namesAndValues[i * 2 + 1] = value;
}
private void value(int i, Object value) {
if (namesAndValues instanceof byte[][]) {
// Reallocate an array of Object.
expand(cap());
}
namesAndValues[i * 2 + 1] = value;
}
private byte[] valueAsBytes(int i) {
Object value = value(i);
if (value instanceof byte[]) {
return (byte[]) value;
} else {
return ((LazyValue<?>) value).toBytes();
}
}
private Object valueAsBytesOrStream(int i) {
Object value = value(i);
if (value instanceof byte[]) {
return value;
} else {
return ((LazyValue<?>) value).toStream();
}
}
private <T> T valueAsT(int i, Key<T> key) {
Object value = value(i);
if (value instanceof byte[]) {
return key.parseBytes((byte[]) value);
} else {
return ((LazyValue<?>) value).toObject(key);
}
}
private int cap() {
return namesAndValues != null ? namesAndValues.length : 0;
}
@ -192,7 +245,7 @@ public final class Metadata {
public <T> T get(Key<T> key) {
for (int i = size - 1; i >= 0; i--) {
if (bytesEqual(key.asciiName(), name(i))) {
return key.parseBytes(value(i));
return valueAsT(i, key);
}
}
return null;
@ -231,7 +284,7 @@ public final class Metadata {
public T next() {
if (hasNext()) {
hasNext = false;
return key.parseBytes(value(idx++));
return valueAsT(idx++, key);
}
throw new NoSuchElementException();
}
@ -288,7 +341,11 @@ public final class Metadata {
Preconditions.checkNotNull(value, "value");
maybeExpand();
name(size, key.asciiName());
value(size, key.toBytes(value));
if (key.serializesToStreams()) {
value(size, LazyValue.create(key, value));
} else {
value(size, key.toBytes(value));
}
size++;
}
@ -300,7 +357,7 @@ public final class Metadata {
// Expands to exactly the desired capacity.
private void expand(int newCapacity) {
byte[][] newNamesAndValues = new byte[newCapacity][];
Object[] newNamesAndValues = new Object[newCapacity];
if (!isEmpty()) {
System.arraycopy(namesAndValues, 0, newNamesAndValues, 0, len());
}
@ -322,8 +379,7 @@ public final class Metadata {
if (!bytesEqual(key.asciiName(), name(i))) {
continue;
}
@SuppressWarnings("unchecked")
T stored = key.parseBytes(value(i));
T stored = valueAsT(i, key);
if (!value.equals(stored)) {
continue;
}
@ -333,7 +389,7 @@ public final class Metadata {
System.arraycopy(namesAndValues, readIdx, namesAndValues, writeIdx, readLen);
size -= 1;
name(size, null);
value(size, null);
value(size, (byte[]) null);
return true;
}
return false;
@ -350,7 +406,7 @@ public final class Metadata {
for (; readIdx < size; readIdx++) {
if (bytesEqual(key.asciiName(), name(readIdx))) {
ret = ret != null ? ret : new ArrayList<T>();
ret.add(key.parseBytes(value(readIdx)));
ret.add(valueAsT(readIdx, key));
continue;
}
name(writeIdx, name(readIdx));
@ -406,11 +462,36 @@ public final class Metadata {
*/
@Nullable
byte[][] serialize() {
if (len() == cap()) {
return namesAndValues;
}
byte[][] serialized = new byte[len()][];
System.arraycopy(namesAndValues, 0, serialized, 0, len());
if (namesAndValues instanceof byte[][]) {
System.arraycopy(namesAndValues, 0, serialized, 0, len());
} else {
for (int i = 0; i < size; i++) {
serialized[i * 2] = name(i);
serialized[i * 2 + 1] = valueAsBytes(i);
}
}
return serialized;
}
/**
* Serializes all metadata entries, leaving some values as {@link InputStream}s.
*
* <p>Produces serialized names and values interleaved. result[i*2] are names, while
* result[i*2+1] are values.
*
* <p>Names are byte arrays as described according to the {@link #serialize}
* method. Values are either byte arrays or {@link InputStream}s.
*
* <p>This method is intended for transport use only.
*/
@Nullable
Object[] serializePartial() {
Object[] serialized = new Object[len()];
for (int i = 0; i < size; i++) {
serialized[i * 2] = name(i);
serialized[i * 2 + 1] = valueAsBytesOrStream(i);
}
return serialized;
}
@ -467,9 +548,9 @@ public final class Metadata {
String headerName = new String(name(i), US_ASCII);
sb.append(headerName).append('=');
if (headerName.endsWith(BINARY_HEADER_SUFFIX)) {
sb.append(BASE64_ENCODING_OMIT_PADDING.encode(value(i)));
sb.append(BASE64_ENCODING_OMIT_PADDING.encode(valueAsBytes(i)));
} else {
String headerValue = new String(value(i), US_ASCII);
String headerValue = new String(valueAsBytes(i), US_ASCII);
sb.append(headerValue);
}
}
@ -532,6 +613,25 @@ public final class Metadata {
T parseAsciiString(String serialized);
}
/** Marshaller for metadata values that are serialized to an InputStream. */
public interface BinaryStreamMarshaller<T> {
/**
* Serializes a metadata value to an {@link InputStream}.
*
* @param value to serialize
* @return serialized version of value
*/
InputStream toStream(T value);
/**
* Parses a serialized metadata value from an {@link InputStream}.
*
* @param stream of metadata to parse
* @return a parsed instance of type T
*/
T parseStream(InputStream stream);
}
/**
* Key for metadata entries. Allows for parsing and serialization of metadata.
*
@ -579,6 +679,16 @@ public final class Metadata {
return new BinaryKey<>(name, marshaller);
}
/**
* Creates a key for a binary header, serializing to input streams.
*
* @param name Must contain only the valid key characters as defined in the class comment. Must
* end with {@link #BINARY_HEADER_SUFFIX}.
*/
public static <T> Key<T> of(String name, BinaryStreamMarshaller<T> marshaller) {
return new LazyStreamBinaryKey<>(name, marshaller);
}
/**
* Creates a key for an ASCII header.
*
@ -601,6 +711,7 @@ public final class Metadata {
private final String name;
private final byte[] nameBytes;
private final Object marshaller;
private static BitSet generateValidTChars() {
BitSet valid = new BitSet(0x7f);
@ -632,10 +743,11 @@ public final class Metadata {
return n;
}
private Key(String name, boolean pseudo) {
private Key(String name, boolean pseudo, Object marshaller) {
this.originalName = checkNotNull(name, "name");
this.name = validateName(this.originalName.toLowerCase(Locale.ROOT), pseudo);
this.nameBytes = this.name.getBytes(US_ASCII);
this.marshaller = marshaller;
}
/**
@ -706,6 +818,28 @@ public final class Metadata {
* @return a parsed instance of type T
*/
abstract T parseBytes(byte[] serialized);
/**
* @return whether this key will be serialized to bytes lazily.
*/
boolean serializesToStreams() {
return false;
}
/**
* Gets this keys (implementation-specific) marshaller, or null if the
* marshaller is not of the given type.
*
* @param marshallerClass The type we expect the marshaller to be.
* @return the marshaller object for this key, or null.
*/
@Nullable
final <M> M getMarshaller(Class<M> marshallerClass) {
if (marshallerClass.isInstance(marshaller)) {
return marshallerClass.cast(marshaller);
}
return null;
}
}
private static class BinaryKey<T> extends Key<T> {
@ -713,7 +847,7 @@ public final class Metadata {
/** Keys have a name and a binary marshaller used for serialization. */
private BinaryKey(String name, BinaryMarshaller<T> marshaller) {
super(name, false /* not pseudo */);
super(name, false /* not pseudo */, marshaller);
checkArgument(
name.endsWith(BINARY_HEADER_SUFFIX),
"Binary header is named %s. It must end with %s",
@ -734,12 +868,93 @@ public final class Metadata {
}
}
/** A binary key for values which should be serialized lazily to {@Link InputStream}s. */
private static class LazyStreamBinaryKey<T> extends Key<T> {
private final BinaryStreamMarshaller<T> marshaller;
/** Keys have a name and a stream marshaller used for serialization. */
private LazyStreamBinaryKey(String name, BinaryStreamMarshaller<T> marshaller) {
super(name, false /* not pseudo */, marshaller);
checkArgument(
name.endsWith(BINARY_HEADER_SUFFIX),
"Binary header is named %s. It must end with %s",
name,
BINARY_HEADER_SUFFIX);
checkArgument(name.length() > BINARY_HEADER_SUFFIX.length(), "empty key name");
this.marshaller = checkNotNull(marshaller, "marshaller is null");
}
@Override
byte[] toBytes(T value) {
return streamToBytes(marshaller.toStream(value));
}
@Override
T parseBytes(byte[] serialized) {
return marshaller.parseStream(new ByteArrayInputStream(serialized));
}
@Override
boolean serializesToStreams() {
return true;
}
}
/** Internal holder for values which are serialized/de-serialized lazily. */
static final class LazyValue<T> {
private final BinaryStreamMarshaller<T> marshaller;
private final T value;
private volatile byte[] serialized;
static <T> LazyValue<T> create(Key<T> key, T value) {
return new LazyValue<>(checkNotNull(getBinaryStreamMarshaller(key)), value);
}
/** A value set by the application. */
LazyValue(BinaryStreamMarshaller<T> marshaller, T value) {
this.marshaller = marshaller;
this.value = value;
}
InputStream toStream() {
return marshaller.toStream(value);
}
byte[] toBytes() {
if (serialized == null) {
synchronized (this) {
if (serialized == null) {
serialized = streamToBytes(toStream());
}
}
}
return serialized;
}
<T2> T2 toObject(Key<T2> key) {
if (key.serializesToStreams()) {
BinaryStreamMarshaller<T2> marshaller = getBinaryStreamMarshaller(key);
if (marshaller != null) {
return marshaller.parseStream(toStream());
}
}
return key.parseBytes(toBytes());
}
@Nullable
@SuppressWarnings("unchecked")
private static <T> BinaryStreamMarshaller<T> getBinaryStreamMarshaller(Key<T> key) {
return (BinaryStreamMarshaller<T>) key.getMarshaller(BinaryStreamMarshaller.class);
}
}
private static class AsciiKey<T> extends Key<T> {
private final AsciiMarshaller<T> marshaller;
/** Keys have a name and an ASCII marshaller used for serialization. */
private AsciiKey(String name, boolean pseudo, AsciiMarshaller<T> marshaller) {
super(name, pseudo);
super(name, pseudo, marshaller);
Preconditions.checkArgument(
!name.endsWith(BINARY_HEADER_SUFFIX),
"ASCII header is named %s. Only binary headers may end with %s",
@ -764,7 +979,7 @@ public final class Metadata {
/** Keys have a name and an ASCII marshaller used for serialization. */
private TrustedAsciiKey(String name, boolean pseudo, TrustedAsciiMarshaller<T> marshaller) {
super(name, pseudo);
super(name, pseudo, marshaller);
Preconditions.checkArgument(
!name.endsWith(BINARY_HEADER_SUFFIX),
"ASCII header is named %s. Only binary headers may end with %s",
@ -808,4 +1023,12 @@ public final class Metadata {
*/
T parseAsciiString(byte[] serialized);
}
private static byte[] streamToBytes(InputStream stream) {
try {
return ByteStreams.toByteArray(stream);
} catch (IOException ioe) {
throw new RuntimeException("failure reading serialized stream", ioe);
}
}
}

View File

@ -22,13 +22,19 @@ import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import com.google.common.collect.Lists;
import com.google.common.io.ByteStreams;
import io.grpc.Metadata.Key;
import io.grpc.internal.GrpcUtil;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Locale;
@ -59,9 +65,59 @@ public class MetadataTest {
}
};
private static class FishStreamMarsaller implements Metadata.BinaryStreamMarshaller<Fish> {
@Override
public InputStream toStream(Fish fish) {
return new ByteArrayInputStream(FISH_MARSHALLER.toBytes(fish));
}
@Override
public Fish parseStream(InputStream stream) {
try {
return FISH_MARSHALLER.parseBytes(ByteStreams.toByteArray(stream));
} catch (IOException ioe) {
throw new AssertionError();
}
}
}
private static final Metadata.BinaryStreamMarshaller<Fish> FISH_STREAM_MARSHALLER =
new FishStreamMarsaller();
/** A pattern commonly used to avoid unnecessary serialization of immutable objects. */
private static final class FakeFishStream extends InputStream {
final Fish fish;
FakeFishStream(Fish fish) {
this.fish = fish;
}
@Override
public int read() throws IOException {
throw new IOException("Not actually a stream");
}
}
private static final Metadata.BinaryStreamMarshaller<Fish> IMMUTABLE_FISH_MARSHALLER =
new Metadata.BinaryStreamMarshaller<Fish>() {
@Override
public InputStream toStream(Fish fish) {
return new FakeFishStream(fish);
}
@Override
public Fish parseStream(InputStream stream) {
return ((FakeFishStream) stream).fish;
}
};
private static final String LANCE = "lance";
private static final byte[] LANCE_BYTES = LANCE.getBytes(US_ASCII);
private static final Metadata.Key<Fish> KEY = Metadata.Key.of("test-bin", FISH_MARSHALLER);
private static final Metadata.Key<Fish> KEY_STREAMED =
Key.of("streamed-bin", FISH_STREAM_MARSHALLER);
private static final Metadata.Key<Fish> KEY_IMMUTABLE =
Key.of("immutable-bin", IMMUTABLE_FISH_MARSHALLER);
@Test
public void noPseudoHeaders() {
@ -334,6 +390,95 @@ public class MetadataTest {
}
}
@Test
public void streamedValue() {
Fish salmon = new Fish("salmon");
Metadata h = new Metadata();
h.put(KEY_STREAMED, salmon);
assertEquals(salmon, h.get(KEY_STREAMED));
}
@Test
public void streamedValueDifferentKey() {
Fish salmon = new Fish("salmon");
Metadata h = new Metadata();
h.put(KEY_STREAMED, salmon);
// Get using a different key instance (but the same marshaller).
Fish fish = h.get(copyKey(KEY_STREAMED, FISH_STREAM_MARSHALLER));
assertEquals(salmon, fish);
}
@Test
public void streamedValueDifferentMarshaller() {
Fish salmon = new Fish("salmon");
Metadata h = new Metadata();
h.put(KEY_STREAMED, salmon);
// Get using a different marshaller instance.
Fish fish = h.get(copyKey(KEY_STREAMED, new FishStreamMarsaller()));
assertEquals(salmon, fish);
}
@Test
public void serializeParseMetadataWithStreams() {
Metadata h = new Metadata();
Fish salmon = new Fish("salmon");
h.put(KEY_STREAMED, salmon);
Metadata parsed = new Metadata(h.serialize());
assertEquals(salmon, parsed.get(KEY_STREAMED));
}
@Test
public void immutableMarshaller() {
Metadata h = new Metadata(KEY.asciiName(), LANCE_BYTES);
Fish salmon = new Fish("salmon");
h.put(KEY_IMMUTABLE, salmon);
assertSame(salmon, h.get(KEY_IMMUTABLE));
// Even though the key differs, the marshaller can chose to avoid serialization.
assertSame(salmon, h.get(copyKey(KEY_IMMUTABLE, IMMUTABLE_FISH_MARSHALLER)));
}
@Test
public void partialSerialization() {
Metadata h = new Metadata(KEY.asciiName(), LANCE_BYTES);
Fish salmon = new Fish("salmon");
h.put(KEY_STREAMED, salmon);
h.put(KEY_IMMUTABLE, salmon);
Object[] serialized = InternalMetadata.serializePartial(h);
assertEquals(6, serialized.length);
assertEquals("test-bin", new String((byte[]) serialized[0], US_ASCII));
assertArrayEquals(LANCE_BYTES, (byte[]) serialized[1]);
assertEquals("streamed-bin", new String((byte[]) serialized[2], US_ASCII));
assertEquals(salmon, FISH_STREAM_MARSHALLER.parseStream((InputStream) serialized[3]));
assertNotSame(salmon, FISH_STREAM_MARSHALLER.parseStream((InputStream) serialized[3]));
assertEquals("immutable-bin", new String((byte[]) serialized[4], US_ASCII));
assertSame(salmon, IMMUTABLE_FISH_MARSHALLER.parseStream((InputStream) serialized[5]));
}
@Test
public void createFromPartial() {
Metadata h = new Metadata(KEY.asciiName(), LANCE_BYTES);
Fish salmon = new Fish("salmon");
h.put(KEY_STREAMED, salmon);
h.put(KEY_IMMUTABLE, salmon);
Fish anotherSalmon = new Fish("salmon");
Object[] partial = InternalMetadata.serializePartial(h);
partial[3] = InternalMetadata.parsedValue(FISH_STREAM_MARSHALLER, anotherSalmon);
partial[5] = InternalMetadata.parsedValue(IMMUTABLE_FISH_MARSHALLER, anotherSalmon);
Metadata h2 = new Metadata(3, partial);
assertEquals(new Fish(LANCE), h2.get(KEY));
assertEquals(anotherSalmon, h2.get(KEY_STREAMED));
assertSame(anotherSalmon, h2.get(KEY_IMMUTABLE));
}
private static final class Fish {
private String name;
@ -366,4 +511,8 @@ public class MetadataTest {
return "Fish(" + name + ")";
}
}
private static <T> Key<T> copyKey(Key<T> key, Metadata.BinaryStreamMarshaller<T> marshaller) {
return Key.of(key.originalName(), marshaller);
}
}