From 19755fbb145dc45a41dbda7a88f202de9ab40343 Mon Sep 17 00:00:00 2001 From: Jon Steinich Date: Fri, 31 Mar 2023 14:10:49 -0500 Subject: [PATCH 1/5] Add copiers for non-IMessage protobuf types --- .../ByteStringCopier.cs | 20 +++++++ .../MapFieldCopier.cs | 59 +++++++++++++++++++ .../RepeatedFieldCopier.cs | 55 +++++++++++++++++ 3 files changed, 134 insertions(+) create mode 100644 src/Serializers/Orleans.Serialization.Protobuf/ByteStringCopier.cs create mode 100644 src/Serializers/Orleans.Serialization.Protobuf/MapFieldCopier.cs create mode 100644 src/Serializers/Orleans.Serialization.Protobuf/RepeatedFieldCopier.cs diff --git a/src/Serializers/Orleans.Serialization.Protobuf/ByteStringCopier.cs b/src/Serializers/Orleans.Serialization.Protobuf/ByteStringCopier.cs new file mode 100644 index 0000000000..449c1025f3 --- /dev/null +++ b/src/Serializers/Orleans.Serialization.Protobuf/ByteStringCopier.cs @@ -0,0 +1,20 @@ +using Google.Protobuf; +using Orleans.Serialization.Cloning; + +namespace Orleans.Serialization; + +[RegisterCopier] +public sealed class ByteStringCopier : IDeepCopier +{ + public ByteString DeepCopy(ByteString input, CopyContext context) + { + if (context.TryGetCopy(input, out var result)) + { + return result; + } + + result = ByteString.CopyFrom(input.Span); + context.RecordCopy(input, result); + return result; + } +} \ No newline at end of file diff --git a/src/Serializers/Orleans.Serialization.Protobuf/MapFieldCopier.cs b/src/Serializers/Orleans.Serialization.Protobuf/MapFieldCopier.cs new file mode 100644 index 0000000000..ca86497f37 --- /dev/null +++ b/src/Serializers/Orleans.Serialization.Protobuf/MapFieldCopier.cs @@ -0,0 +1,59 @@ +using Google.Protobuf.Collections; +using Orleans.Serialization.Cloning; + +namespace Orleans.Serialization; + +/// +/// Copier for . +/// +/// The type of the t key. +/// The type of the t value. +[RegisterCopier] +public sealed class MapFieldCopier : IDeepCopier>, IBaseCopier> +{ + private readonly IDeepCopier _keyCopier; + private readonly IDeepCopier _valueCopier; + + /// + /// Initializes a new instance of the class. + /// + /// The key copier. + /// The value copier. + public MapFieldCopier(IDeepCopier keyCopier, IDeepCopier valueCopier) + { + _keyCopier = keyCopier; + _valueCopier = valueCopier; + } + + /// + public MapField DeepCopy(MapField input, CopyContext context) + { + if (context.TryGetCopy>(input, out var result)) + { + return result; + } + + if (input.GetType() != typeof(MapField)) + { + return context.DeepCopy(input); + } + + result = new MapField(); + context.RecordCopy(input, result); + foreach (var pair in input) + { + result[_keyCopier.DeepCopy(pair.Key, context)] = _valueCopier.DeepCopy(pair.Value, context); + } + + return result; + } + + /// + public void DeepCopy(MapField input, MapField output, CopyContext context) + { + foreach (var pair in input) + { + output[_keyCopier.DeepCopy(pair.Key, context)] = _valueCopier.DeepCopy(pair.Value, context); + } + } +} \ No newline at end of file diff --git a/src/Serializers/Orleans.Serialization.Protobuf/RepeatedFieldCopier.cs b/src/Serializers/Orleans.Serialization.Protobuf/RepeatedFieldCopier.cs new file mode 100644 index 0000000000..61aae9ea8e --- /dev/null +++ b/src/Serializers/Orleans.Serialization.Protobuf/RepeatedFieldCopier.cs @@ -0,0 +1,55 @@ +using Google.Protobuf.Collections; +using Orleans.Serialization.Cloning; + +namespace Orleans.Serialization; + +/// +/// Copier for . +/// +/// The element type. +[RegisterCopier] +public sealed class RepeatedFieldCopier : IDeepCopier>, IBaseCopier> +{ + private readonly IDeepCopier _copier; + + /// + /// Initializes a new instance of the class. + /// + /// The value copier. + public RepeatedFieldCopier(IDeepCopier valueCopier) + { + _copier = valueCopier; + } + + /// + public RepeatedField DeepCopy(RepeatedField input, CopyContext context) + { + if (context.TryGetCopy>(input, out var result)) + { + return result; + } + + if (input.GetType() != typeof(RepeatedField)) + { + return context.DeepCopy(input); + } + + result = new RepeatedField { Capacity = input.Count }; + context.RecordCopy(input, result); + foreach (var item in input) + { + result.Add(_copier.DeepCopy(item, context)); + } + + return result; + } + + /// + public void DeepCopy(RepeatedField input, RepeatedField output, CopyContext context) + { + foreach (var item in input) + { + output.Add(_copier.DeepCopy(item, context)); + } + } +} \ No newline at end of file From af50c6cab3a69283ba1f5ff206fcf6d48d8e6881 Mon Sep 17 00:00:00 2001 From: Jon Steinich Date: Fri, 31 Mar 2023 14:48:42 -0500 Subject: [PATCH 2/5] add tests --- .../ProtobufSerializerTests.cs | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/test/Orleans.Serialization.UnitTests/ProtobufSerializerTests.cs b/test/Orleans.Serialization.UnitTests/ProtobufSerializerTests.cs index 0ae491fdba..54636cc8cb 100644 --- a/test/Orleans.Serialization.UnitTests/ProtobufSerializerTests.cs +++ b/test/Orleans.Serialization.UnitTests/ProtobufSerializerTests.cs @@ -1,6 +1,8 @@ #nullable enable using System; +using System.Linq; using Google.Protobuf; +using Google.Protobuf.Collections; using Microsoft.Extensions.DependencyInjection; using Orleans.Serialization.Cloning; using Orleans.Serialization.Codecs; @@ -101,6 +103,71 @@ protected override void Configure(ISerializerBuilder builder) }; } +[Trait("Category", "BVT")] +public class ProtobufRepeatedFieldCopierTests : CopierTester, IDeepCopier>> +{ + public ProtobufRepeatedFieldCopierTests(ITestOutputHelper output) : base(output) + { + } + + protected override IDeepCopier> CreateCopier() => ServiceProvider.GetRequiredService().GetDeepCopier>(); + + protected override RepeatedField CreateValue() + { + var result = new RepeatedField(); + for (var i = 0; i < Random.Next(17) + 5; i++) + { + result.Add(Random.Next()); + } + + return result; + } + + protected override bool Equals(RepeatedField left, RepeatedField right) => object.ReferenceEquals(left, right) || left.SequenceEqual(right); + protected override RepeatedField[] TestValues => new[] { new RepeatedField(), CreateValue(), CreateValue(), CreateValue() }; +} + +[Trait("Category", "BVT")] +public class MapFieldCopierTests : CopierTester, MapFieldCopier> +{ + public MapFieldCopierTests(ITestOutputHelper output) : base(output) + { + } + + protected override MapField CreateValue() + { + var result = new MapField(); + for (var i = 0; i < Random.Next(17) + 5; i++) + { + result[Random.Next().ToString()] = Random.Next(); + } + + return result; + } + + protected override MapField[] TestValues => new[] { new MapField(), CreateValue(), CreateValue(), CreateValue() }; + protected override bool Equals(MapField left, MapField right) => object.ReferenceEquals(left, right) || left.SequenceEqual(right); +} + +[Trait("Category", "BVT")] +public class ByteStringCopierTests : CopierTester +{ + public ByteStringCopierTests(ITestOutputHelper output) : base(output) + { + } + + protected override ByteString CreateValue() => Guid.NewGuid().ToByteString(); + + protected override bool Equals(ByteString left, ByteString right) => ReferenceEquals(left, right) || left.SequenceEqual(right); + + protected override ByteString[] TestValues => new[] + { + ByteString.Empty, + ByteString.CopyFrom(Enumerable.Range(0, 4097).Select(b => unchecked((byte)b)).ToArray()), + CreateValue() + }; +} + public static class ProtobufGuidExtensions { public static ByteString ToByteString(this Guid guid) From 90c9743260c16e008f80b2a69506c6540531283e Mon Sep 17 00:00:00 2001 From: Jon Steinich Date: Fri, 31 Mar 2023 15:38:00 -0500 Subject: [PATCH 3/5] Some code doc cleanup --- .../Orleans.Serialization.Protobuf/ByteStringCopier.cs | 4 ++++ .../Orleans.Serialization.Protobuf/MapFieldCopier.cs | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Serializers/Orleans.Serialization.Protobuf/ByteStringCopier.cs b/src/Serializers/Orleans.Serialization.Protobuf/ByteStringCopier.cs index 449c1025f3..3f96ea306b 100644 --- a/src/Serializers/Orleans.Serialization.Protobuf/ByteStringCopier.cs +++ b/src/Serializers/Orleans.Serialization.Protobuf/ByteStringCopier.cs @@ -3,9 +3,13 @@ namespace Orleans.Serialization; +/// +/// Copier for . +/// [RegisterCopier] public sealed class ByteStringCopier : IDeepCopier { + /// public ByteString DeepCopy(ByteString input, CopyContext context) { if (context.TryGetCopy(input, out var result)) diff --git a/src/Serializers/Orleans.Serialization.Protobuf/MapFieldCopier.cs b/src/Serializers/Orleans.Serialization.Protobuf/MapFieldCopier.cs index ca86497f37..9d1093a885 100644 --- a/src/Serializers/Orleans.Serialization.Protobuf/MapFieldCopier.cs +++ b/src/Serializers/Orleans.Serialization.Protobuf/MapFieldCopier.cs @@ -4,7 +4,7 @@ namespace Orleans.Serialization; /// -/// Copier for . +/// Copier for . /// /// The type of the t key. /// The type of the t value. From 650862c5767b1dd429355c2f6da31524f3e00c05 Mon Sep 17 00:00:00 2001 From: Jon Steinich Date: Mon, 3 Apr 2023 15:21:06 -0500 Subject: [PATCH 4/5] Add codecs as well --- .../ByteStringCodec.cs | 42 ++++++ .../MapFieldCodec.cs | 135 ++++++++++++++++++ .../RepeatedFieldCodec.cs | 118 +++++++++++++++ 3 files changed, 295 insertions(+) create mode 100644 src/Serializers/Orleans.Serialization.Protobuf/ByteStringCodec.cs create mode 100644 src/Serializers/Orleans.Serialization.Protobuf/MapFieldCodec.cs create mode 100644 src/Serializers/Orleans.Serialization.Protobuf/RepeatedFieldCodec.cs diff --git a/src/Serializers/Orleans.Serialization.Protobuf/ByteStringCodec.cs b/src/Serializers/Orleans.Serialization.Protobuf/ByteStringCodec.cs new file mode 100644 index 0000000000..561ae06bcf --- /dev/null +++ b/src/Serializers/Orleans.Serialization.Protobuf/ByteStringCodec.cs @@ -0,0 +1,42 @@ +using System; +using Google.Protobuf; +using Orleans.Serialization.Buffers; +using Orleans.Serialization.Codecs; +using Orleans.Serialization.WireProtocol; + +namespace Orleans.Serialization; + +/// +/// Serializer for . +/// +[RegisterSerializer] +public sealed class ByteStringCodec : IFieldCodec +{ + /// + ByteString IFieldCodec.ReadValue(ref Reader reader, Field field) + { + if (field.WireType == WireType.Reference) + { + return ReferenceCodec.ReadReference(ref reader, field); + } + + field.EnsureWireType(WireType.LengthPrefixed); + var length = reader.ReadVarUInt32(); + var result = UnsafeByteOperations.UnsafeWrap(reader.ReadBytes(length)); + ReferenceCodec.RecordObject(reader.Session, result); + return result; + } + + /// + void IFieldCodec.WriteField(ref Writer writer, uint fieldIdDelta, Type expectedType, ByteString value) + { + if (ReferenceCodec.TryWriteReferenceField(ref writer, fieldIdDelta, expectedType, value)) + { + return; + } + + writer.WriteFieldHeader(fieldIdDelta, expectedType, typeof(ByteString), WireType.LengthPrefixed); + writer.WriteVarUInt32((uint)value.Length); + writer.Write(value.Span); + } +} \ No newline at end of file diff --git a/src/Serializers/Orleans.Serialization.Protobuf/MapFieldCodec.cs b/src/Serializers/Orleans.Serialization.Protobuf/MapFieldCodec.cs new file mode 100644 index 0000000000..5ca1fe953a --- /dev/null +++ b/src/Serializers/Orleans.Serialization.Protobuf/MapFieldCodec.cs @@ -0,0 +1,135 @@ +using System; +using System.Buffers; +using Google.Protobuf.Collections; +using Orleans.Serialization.Buffers; +using Orleans.Serialization.Codecs; +using Orleans.Serialization.GeneratedCodeHelpers; +using Orleans.Serialization.Session; +using Orleans.Serialization.WireProtocol; + +namespace Orleans.Serialization; + +/// +/// Serializer for . +/// +/// The key type. +/// The value type. +[RegisterSerializer] +public sealed class MapFieldCodec : IFieldCodec> +{ + private readonly Type _keyFieldType = typeof(TKey); + private readonly Type _valueFieldType = typeof(TValue); + + private readonly IFieldCodec _keyCodec; + private readonly IFieldCodec _valueCodec; + + /// + /// Initializes a new instance of the class. + /// + /// The key codec. + /// The value codec. + public MapFieldCodec( + IFieldCodec keyCodec, + IFieldCodec valueCodec) + { + _keyCodec = OrleansGeneratedCodeHelper.UnwrapService(this, keyCodec); + _valueCodec = OrleansGeneratedCodeHelper.UnwrapService(this, valueCodec); + } + + /// + public void WriteField(ref Writer writer, uint fieldIdDelta, Type expectedType, MapField value) where TBufferWriter : IBufferWriter + { + if (ReferenceCodec.TryWriteReferenceField(ref writer, fieldIdDelta, expectedType, value)) + { + return; + } + + writer.WriteFieldHeader(fieldIdDelta, expectedType, value.GetType(), WireType.TagDelimited); + + if (value.Count > 0) + { + UInt32Codec.WriteField(ref writer, 0, (uint)value.Count); + uint innerFieldIdDelta = 1; + foreach (var element in value) + { + _keyCodec.WriteField(ref writer, innerFieldIdDelta, _keyFieldType, element.Key); + _valueCodec.WriteField(ref writer, 0, _valueFieldType, element.Value); + innerFieldIdDelta = 0; + } + } + + writer.WriteEndObject(); + } + + /// + public MapField ReadValue(ref Reader reader, Field field) + { + if (field.WireType == WireType.Reference) + { + return ReferenceCodec.ReadReference, TInput>(ref reader, field); + } + + field.EnsureWireTypeTagDelimited(); + + var placeholderReferenceId = ReferenceCodec.CreateRecordPlaceholder(reader.Session); + TKey key = default; + var valueExpected = false; + MapField result = null; + uint fieldId = 0; + while (true) + { + var header = reader.ReadFieldHeader(); + if (header.IsEndBaseOrEndObject) + { + break; + } + + fieldId += header.FieldIdDelta; + switch (fieldId) + { + case 0: + var length = (int)UInt32Codec.ReadValue(ref reader, header); + if (length > 10240 && length > reader.Length) + { + ThrowInvalidSizeException(length); + } + + result = CreateInstance(reader.Session, placeholderReferenceId); + break; + case 1: + if (result is null) + ThrowLengthFieldMissing(); + + if (!valueExpected) + { + key = _keyCodec.ReadValue(ref reader, header); + valueExpected = true; + } + else + { + result.Add(key, _valueCodec.ReadValue(ref reader, header)); + valueExpected = false; + } + break; + default: + reader.ConsumeUnknownField(header); + break; + } + } + + result ??= CreateInstance(reader.Session, placeholderReferenceId); + return result; + } + + private static MapField CreateInstance(SerializerSession session, uint placeholderReferenceId) + { + var result = new MapField(); + ReferenceCodec.RecordObject(session, result, placeholderReferenceId); + return result; + } + + private static void ThrowInvalidSizeException(int length) => throw new IndexOutOfRangeException( + $"Declared length of {typeof(MapField)}, {length}, is greater than total length of input."); + + private static void ThrowLengthFieldMissing() => throw new RequiredFieldMissingException("Serialized MapField is missing its length field."); +} \ No newline at end of file diff --git a/src/Serializers/Orleans.Serialization.Protobuf/RepeatedFieldCodec.cs b/src/Serializers/Orleans.Serialization.Protobuf/RepeatedFieldCodec.cs new file mode 100644 index 0000000000..909cc8fca5 --- /dev/null +++ b/src/Serializers/Orleans.Serialization.Protobuf/RepeatedFieldCodec.cs @@ -0,0 +1,118 @@ +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using Google.Protobuf.Collections; +using Orleans.Serialization.Buffers; +using Orleans.Serialization.Codecs; +using Orleans.Serialization.GeneratedCodeHelpers; +using Orleans.Serialization.WireProtocol; + +namespace Orleans.Serialization; + +/// +/// Serializer for . +/// +/// The element type. +[RegisterSerializer] +public sealed class RepeatedFieldCodec : IFieldCodec> +{ + private readonly Type CodecElementType = typeof(T); + + private readonly IFieldCodec _fieldCodec; + + /// + /// Initializes a new instance of the class. + /// + /// The field codec. + public RepeatedFieldCodec(IFieldCodec fieldCodec) + { + _fieldCodec = OrleansGeneratedCodeHelper.UnwrapService(this, fieldCodec); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteField(ref Writer writer, uint fieldIdDelta, Type expectedType, RepeatedField value) where TBufferWriter : IBufferWriter + { + if (ReferenceCodec.TryWriteReferenceField(ref writer, fieldIdDelta, expectedType, value)) + { + return; + } + + writer.WriteFieldHeader(fieldIdDelta, expectedType, value.GetType(), WireType.TagDelimited); + + if (value.Count > 0) + { + UInt32Codec.WriteField(ref writer, 0, (uint)value.Count); + uint innerFieldIdDelta = 1; + foreach (var element in value) + { + _fieldCodec.WriteField(ref writer, innerFieldIdDelta, CodecElementType, element); + innerFieldIdDelta = 0; + } + } + + writer.WriteEndObject(); + } + + /// + public RepeatedField ReadValue(ref Reader reader, Field field) + { + if (field.WireType == WireType.Reference) + { + return ReferenceCodec.ReadReference, TInput>(ref reader, field); + } + + field.EnsureWireTypeTagDelimited(); + + var placeholderReferenceId = ReferenceCodec.CreateRecordPlaceholder(reader.Session); + RepeatedField result = null; + uint fieldId = 0; + while (true) + { + var header = reader.ReadFieldHeader(); + if (header.IsEndBaseOrEndObject) + { + break; + } + + fieldId += header.FieldIdDelta; + switch (fieldId) + { + case 0: + var length = (int)UInt32Codec.ReadValue(ref reader, header); + if (length > 10240 && length > reader.Length) + { + ThrowInvalidSizeException(length); + } + + result = new RepeatedField{ Capacity = length }; + ReferenceCodec.RecordObject(reader.Session, result, placeholderReferenceId); + break; + case 1: + if (result is null) + { + ThrowLengthFieldMissing(); + } + + result.Add(_fieldCodec.ReadValue(ref reader, header)); + break; + default: + reader.ConsumeUnknownField(header); + break; + } + } + + if (result is null) + { + result = new(); + ReferenceCodec.RecordObject(reader.Session, result, placeholderReferenceId); + } + + return result; + } + + private static void ThrowInvalidSizeException(int length) => throw new IndexOutOfRangeException( + $"Declared length of {typeof(RepeatedField)}, {length}, is greater than total length of input."); + + private static void ThrowLengthFieldMissing() => throw new RequiredFieldMissingException("Serialized RepeatedField is missing its length field."); +} \ No newline at end of file From f59f00d2eac51693b7ead8190b1894497d211efb Mon Sep 17 00:00:00 2001 From: Jon Steinich Date: Mon, 3 Apr 2023 15:28:13 -0500 Subject: [PATCH 5/5] add codec tests --- .../ProtobufSerializerTests.cs | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/test/Orleans.Serialization.UnitTests/ProtobufSerializerTests.cs b/test/Orleans.Serialization.UnitTests/ProtobufSerializerTests.cs index 54636cc8cb..5ff46c04e8 100644 --- a/test/Orleans.Serialization.UnitTests/ProtobufSerializerTests.cs +++ b/test/Orleans.Serialization.UnitTests/ProtobufSerializerTests.cs @@ -103,6 +103,28 @@ protected override void Configure(ISerializerBuilder builder) }; } +[Trait("Category", "BVT")] +public class ProtobufRepeatedFieldCodecTests : FieldCodecTester, RepeatedFieldCodec> +{ + public ProtobufRepeatedFieldCodecTests(ITestOutputHelper output) : base(output) + { + } + + protected override RepeatedField CreateValue() + { + var result = new RepeatedField(); + for (var i = 0; i < Random.Next(17) + 5; i++) + { + result.Add(Random.Next()); + } + + return result; + } + + protected override bool Equals(RepeatedField left, RepeatedField right) => object.ReferenceEquals(left, right) || left.SequenceEqual(right); + protected override RepeatedField[] TestValues => new[] { new RepeatedField(), CreateValue(), CreateValue(), CreateValue() }; +} + [Trait("Category", "BVT")] public class ProtobufRepeatedFieldCopierTests : CopierTester, IDeepCopier>> { @@ -127,6 +149,28 @@ protected override RepeatedField CreateValue() protected override RepeatedField[] TestValues => new[] { new RepeatedField(), CreateValue(), CreateValue(), CreateValue() }; } +[Trait("Category", "BVT")] +public class MapFieldCodecTests : FieldCodecTester, MapFieldCodec> +{ + public MapFieldCodecTests(ITestOutputHelper output) : base(output) + { + } + + protected override MapField CreateValue() + { + var result = new MapField(); + for (var i = 0; i < Random.Next(17) + 5; i++) + { + result[Random.Next().ToString()] = Random.Next(); + } + + return result; + } + + protected override MapField[] TestValues => new[] { new MapField(), CreateValue(), CreateValue(), CreateValue() }; + protected override bool Equals(MapField left, MapField right) => object.ReferenceEquals(left, right) || left.SequenceEqual(right); +} + [Trait("Category", "BVT")] public class MapFieldCopierTests : CopierTester, MapFieldCopier> { @@ -149,6 +193,25 @@ protected override MapField CreateValue() protected override bool Equals(MapField left, MapField right) => object.ReferenceEquals(left, right) || left.SequenceEqual(right); } +[Trait("Category", "BVT")] +public class ByteStringCodecTests : FieldCodecTester +{ + public ByteStringCodecTests(ITestOutputHelper output) : base(output) + { + } + + protected override ByteString CreateValue() => Guid.NewGuid().ToByteString(); + + protected override bool Equals(ByteString left, ByteString right) => ReferenceEquals(left, right) || left.SequenceEqual(right); + + protected override ByteString[] TestValues => new[] + { + ByteString.Empty, + ByteString.CopyFrom(Enumerable.Range(0, 4097).Select(b => unchecked((byte)b)).ToArray()), + CreateValue() + }; +} + [Trait("Category", "BVT")] public class ByteStringCopierTests : CopierTester {