Эх сурвалжийг харах

Merge pull request #16367 from jtattermusch/csharp_new_serialization_api

Add new C# serialization API
Jan Tattermusch 7 жил өмнө
parent
commit
d4356bf719

+ 119 - 0
src/csharp/Grpc.Core.Tests/ContextualMarshallerTest.cs

@@ -0,0 +1,119 @@
+#region Copyright notice and license
+
+// Copyright 2018 The gRPC Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#endregion
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+using Grpc.Core;
+using Grpc.Core.Internal;
+using Grpc.Core.Utils;
+using NUnit.Framework;
+
+namespace Grpc.Core.Tests
+{
+    public class ContextualMarshallerTest
+    {
+        const string Host = "127.0.0.1";
+
+        MockServiceHelper helper;
+        Server server;
+        Channel channel;
+
+        [SetUp]
+        public void Init()
+        {
+            var contextualMarshaller = new Marshaller<string>(
+                (str, serializationContext) =>
+                {
+                    if (str == "UNSERIALIZABLE_VALUE")
+                    {
+                        // Google.Protobuf throws exception inherited from IOException
+                        throw new IOException("Error serializing the message.");
+                    }
+                    if (str == "SERIALIZE_TO_NULL")
+                    {
+                        return;
+                    }
+                    var bytes = System.Text.Encoding.UTF8.GetBytes(str);
+                    serializationContext.Complete(bytes);
+                },
+                (deserializationContext) =>
+                {
+                    var buffer = deserializationContext.PayloadAsNewBuffer();
+                    Assert.AreEqual(buffer.Length, deserializationContext.PayloadLength);
+                    var s = System.Text.Encoding.UTF8.GetString(buffer);
+                    if (s == "UNPARSEABLE_VALUE")
+                    {
+                        // Google.Protobuf throws exception inherited from IOException
+                        throw new IOException("Error parsing the message.");
+                    }
+                    return s;
+                });
+            helper = new MockServiceHelper(Host, contextualMarshaller);
+            server = helper.GetServer();
+            server.Start();
+            channel = helper.GetChannel();
+        }
+
+        [TearDown]
+        public void Cleanup()
+        {
+            channel.ShutdownAsync().Wait();
+            server.ShutdownAsync().Wait();
+        }
+
+        [Test]
+        public void UnaryCall()
+        {
+            helper.UnaryHandler = new UnaryServerMethod<string, string>((request, context) =>
+            {
+                return Task.FromResult(request);
+            });
+            Assert.AreEqual("ABC", Calls.BlockingUnaryCall(helper.CreateUnaryCall(), "ABC"));
+        }
+
+        [Test]
+        public void ResponseParsingError_UnaryResponse()
+        {
+            helper.UnaryHandler = new UnaryServerMethod<string, string>((request, context) =>
+            {
+                return Task.FromResult("UNPARSEABLE_VALUE");
+            });
+
+            var ex = Assert.Throws<RpcException>(() => Calls.BlockingUnaryCall(helper.CreateUnaryCall(), "REQUEST"));
+            Assert.AreEqual(StatusCode.Internal, ex.Status.StatusCode);
+        }
+
+        [Test]
+        public void RequestSerializationError_BlockingUnary()
+        {
+            Assert.Throws<IOException>(() => Calls.BlockingUnaryCall(helper.CreateUnaryCall(), "UNSERIALIZABLE_VALUE"));
+        }
+
+        [Test]
+        public void SerializationResultIsNull_BlockingUnary()
+        {
+            Assert.Throws<NullReferenceException>(() => Calls.BlockingUnaryCall(helper.CreateUnaryCall(), "SERIALIZE_TO_NULL"));
+        }
+    }
+}

+ 105 - 0
src/csharp/Grpc.Core.Tests/MarshallerTest.cs

@@ -0,0 +1,105 @@
+#region Copyright notice and license
+
+// Copyright 2018 The gRPC Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#endregion
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+using Grpc.Core;
+using Grpc.Core.Internal;
+using Grpc.Core.Utils;
+using NUnit.Framework;
+
+namespace Grpc.Core.Tests
+{
+    public class MarshallerTest
+    {
+        [Test]
+        public void ContextualSerializerEmulation()
+        {
+            Func<string, byte[]> simpleSerializer = System.Text.Encoding.UTF8.GetBytes;
+            Func<byte[], string> simpleDeserializer = System.Text.Encoding.UTF8.GetString;
+            var marshaller = new Marshaller<string>(simpleSerializer,
+                                                    simpleDeserializer);
+
+            Assert.AreSame(simpleSerializer, marshaller.Serializer);
+            Assert.AreSame(simpleDeserializer, marshaller.Deserializer);
+
+            // test that emulated contextual serializer and deserializer work
+            string origMsg = "abc";
+            var serializationContext = new FakeSerializationContext();
+            marshaller.ContextualSerializer(origMsg, serializationContext);
+
+            var deserializationContext = new FakeDeserializationContext(serializationContext.Payload);
+            Assert.AreEqual(origMsg, marshaller.ContextualDeserializer(deserializationContext));
+        }
+
+        [Test]
+        public void SimpleSerializerEmulation()
+        {
+            Action<string, SerializationContext> contextualSerializer = (str, context) =>
+            {
+                var bytes = System.Text.Encoding.UTF8.GetBytes(str);
+                context.Complete(bytes);
+            };
+            Func<DeserializationContext, string> contextualDeserializer = (context) =>
+            {
+                return System.Text.Encoding.UTF8.GetString(context.PayloadAsNewBuffer());
+            };
+            var marshaller = new Marshaller<string>(contextualSerializer, contextualDeserializer);
+
+            Assert.AreSame(contextualSerializer, marshaller.ContextualSerializer);
+            Assert.AreSame(contextualDeserializer, marshaller.ContextualDeserializer);
+
+            // test that emulated serializer and deserializer work
+            var origMsg = "abc";
+            var serialized = marshaller.Serializer(origMsg);
+            Assert.AreEqual(origMsg, marshaller.Deserializer(serialized));
+        }
+
+        class FakeSerializationContext : SerializationContext
+        {
+            public byte[] Payload;
+            public override void Complete(byte[] payload)
+            {
+                this.Payload = payload;
+            }
+        }
+
+        class FakeDeserializationContext : DeserializationContext
+        {
+            public byte[] payload;
+
+            public FakeDeserializationContext(byte[] payload)
+            {
+                this.payload = payload;
+            }
+
+            public override int PayloadLength => payload.Length;
+
+            public override byte[] PayloadAsNewBuffer()
+            {
+                return payload;
+            }
+        }
+    }
+}

+ 46 - 0
src/csharp/Grpc.Core/DeserializationContext.cs

@@ -0,0 +1,46 @@
+#region Copyright notice and license
+
+// Copyright 2018 The gRPC Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#endregion
+
+namespace Grpc.Core
+{
+    /// <summary>
+    /// Provides access to the payload being deserialized when deserializing messages.
+    /// </summary>
+    public abstract class DeserializationContext
+    {
+        /// <summary>
+        /// Get the total length of the payload in bytes.
+        /// </summary>
+        public abstract int PayloadLength { get; }
+
+        /// <summary>
+        /// Gets the entire payload as a newly allocated byte array.
+        /// Once the byte array is returned, the byte array becomes owned by the caller and won't be ever accessed or reused by gRPC again.
+        /// NOTE: Obtaining the buffer as a newly allocated byte array is the simplest way of accessing the payload,
+        /// but it can have important consequences in high-performance scenarios.
+        /// In particular, using this method usually requires copying of the entire buffer one extra time.
+        /// Also, allocating a new buffer each time can put excessive pressure on GC, especially if
+        /// the payload is more than 86700 bytes large (which means the newly allocated buffer will be placed in LOH,
+        /// and LOH object can only be garbage collected via a full ("stop the world") GC run).
+        /// NOTE: Deserializers are expected not to call this method more than once per received message
+        /// (as there is no practical reason for doing so) and <c>DeserializationContext</c> implementations are free to assume so.
+        /// </summary>
+        /// <returns>byte array containing the entire payload.</returns>
+        public abstract byte[] PayloadAsNewBuffer();
+    }
+}

+ 114 - 12
src/csharp/Grpc.Core/Marshaller.cs

@@ -29,36 +29,129 @@ namespace Grpc.Core
         readonly Func<T, byte[]> serializer;
         readonly Func<byte[], T> deserializer;
 
+        readonly Action<T, SerializationContext> contextualSerializer;
+        readonly Func<DeserializationContext, T> contextualDeserializer;
+
         /// <summary>
-        /// Initializes a new marshaller.
+        /// Initializes a new marshaller from simple serialize/deserialize functions.
         /// </summary>
         /// <param name="serializer">Function that will be used to serialize messages.</param>
         /// <param name="deserializer">Function that will be used to deserialize messages.</param>
         public Marshaller(Func<T, byte[]> serializer, Func<byte[], T> deserializer)
         {
-            this.serializer = GrpcPreconditions.CheckNotNull(serializer, "serializer");
-            this.deserializer = GrpcPreconditions.CheckNotNull(deserializer, "deserializer");
+            this.serializer = GrpcPreconditions.CheckNotNull(serializer, nameof(serializer));
+            this.deserializer = GrpcPreconditions.CheckNotNull(deserializer, nameof(deserializer));
+            this.contextualSerializer = EmulateContextualSerializer;
+            this.contextualDeserializer = EmulateContextualDeserializer;
         }
 
         /// <summary>
-        /// Gets the serializer function.
+        /// Initializes a new marshaller from serialize/deserialize fuctions that can access serialization and deserialization
+        /// context. Compared to the simple serializer/deserializer functions, using the contextual version provides more
+        /// flexibility and can lead to increased efficiency (and better performance).
+        /// Note: This constructor is part of an experimental API that can change or be removed without any prior notice.
         /// </summary>
-        public Func<T, byte[]> Serializer
+        /// <param name="serializer">Function that will be used to serialize messages.</param>
+        /// <param name="deserializer">Function that will be used to deserialize messages.</param>
+        public Marshaller(Action<T, SerializationContext> serializer, Func<DeserializationContext, T> deserializer)
         {
-            get
-            {
-                return this.serializer;
-            }
+            this.contextualSerializer = GrpcPreconditions.CheckNotNull(serializer, nameof(serializer));
+            this.contextualDeserializer = GrpcPreconditions.CheckNotNull(deserializer, nameof(deserializer));
+            // TODO(jtattermusch): once gRPC C# library switches to using contextual (de)serializer,
+            // emulating the simple (de)serializer will become unnecessary.
+            this.serializer = EmulateSimpleSerializer;
+            this.deserializer = EmulateSimpleDeserializer;
         }
 
+        /// <summary>
+        /// Gets the serializer function.
+        /// </summary>
+        public Func<T, byte[]> Serializer => this.serializer;
+
         /// <summary>
         /// Gets the deserializer function.
         /// </summary>
-        public Func<byte[], T> Deserializer
+        public Func<byte[], T> Deserializer => this.deserializer;
+
+        /// <summary>
+        /// Gets the serializer function.
+        /// Note: experimental API that can change or be removed without any prior notice.
+        /// </summary>
+        public Action<T, SerializationContext> ContextualSerializer => this.contextualSerializer;
+
+        /// <summary>
+        /// Gets the serializer function.
+        /// Note: experimental API that can change or be removed without any prior notice.
+        /// </summary>
+        public Func<DeserializationContext, T> ContextualDeserializer => this.contextualDeserializer;
+
+        // for backward compatibility, emulate the simple serializer using the contextual one
+        private byte[] EmulateSimpleSerializer(T msg)
         {
-            get
+            // TODO(jtattermusch): avoid the allocation by passing a thread-local instance
+            // This code will become unnecessary once gRPC C# library switches to using contextual (de)serializer.
+            var context = new EmulatedSerializationContext();
+            this.contextualSerializer(msg, context);
+            return context.GetPayload();
+        }
+
+        // for backward compatibility, emulate the simple deserializer using the contextual one
+        private T EmulateSimpleDeserializer(byte[] payload)
+        {
+            // TODO(jtattermusch): avoid the allocation by passing a thread-local instance
+            // This code will become unnecessary once gRPC C# library switches to using contextual (de)serializer.
+            var context = new EmulatedDeserializationContext(payload);
+            return this.contextualDeserializer(context);
+        }
+
+        // for backward compatibility, emulate the contextual serializer using the simple one
+        private void EmulateContextualSerializer(T message, SerializationContext context)
+        {
+            var payload = this.serializer(message);
+            context.Complete(payload);
+        }
+
+        // for backward compatibility, emulate the contextual deserializer using the simple one
+        private T EmulateContextualDeserializer(DeserializationContext context)
+        {
+            return this.deserializer(context.PayloadAsNewBuffer());
+        }
+
+        internal class EmulatedSerializationContext : SerializationContext
+        {
+            bool isComplete;
+            byte[] payload;
+
+            public override void Complete(byte[] payload)
+            {
+                GrpcPreconditions.CheckState(!isComplete);
+                this.isComplete = true;
+                this.payload = payload;
+            }
+
+            internal byte[] GetPayload()
+            {
+                return this.payload;
+            }
+        }
+
+        internal class EmulatedDeserializationContext : DeserializationContext
+        {
+            readonly byte[] payload;
+            bool alreadyCalledPayloadAsNewBuffer;
+
+            public EmulatedDeserializationContext(byte[] payload)
+            {
+                this.payload = GrpcPreconditions.CheckNotNull(payload);
+            }
+
+            public override int PayloadLength => payload.Length;
+
+            public override byte[] PayloadAsNewBuffer()
             {
-                return this.deserializer;
+                GrpcPreconditions.CheckState(!alreadyCalledPayloadAsNewBuffer);
+                alreadyCalledPayloadAsNewBuffer = true;
+                return payload;
             }
         }
     }
@@ -76,6 +169,15 @@ namespace Grpc.Core
             return new Marshaller<T>(serializer, deserializer);
         }
 
+        /// <summary>
+        /// Creates a marshaller from specified contextual serializer and deserializer.
+        /// Note: This method is part of an experimental API that can change or be removed without any prior notice.
+        /// </summary>
+        public static Marshaller<T> Create<T>(Action<T, SerializationContext> serializer, Func<DeserializationContext, T> deserializer)
+        {
+            return new Marshaller<T>(serializer, deserializer);
+        }
+
         /// <summary>
         /// Returns a marshaller for <c>string</c> type. This is useful for testing.
         /// </summary>

+ 34 - 0
src/csharp/Grpc.Core/SerializationContext.cs

@@ -0,0 +1,34 @@
+#region Copyright notice and license
+
+// Copyright 2018 The gRPC Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#endregion
+
+namespace Grpc.Core
+{
+    /// <summary>
+    /// Provides storage for payload when serializing a message.
+    /// </summary>
+    public abstract class SerializationContext
+    {
+        /// <summary>
+        /// Use the byte array as serialized form of current message and mark serialization process as complete.
+        /// Complete() can only be called once. By calling this method the caller gives up the ownership of the
+        /// payload which must not be accessed afterwards.
+        /// </summary>
+        /// <param name="payload">the serialized form of current message</param>
+        public abstract void Complete(byte[] payload);
+    }
+}

+ 2 - 0
src/csharp/tests.json

@@ -23,8 +23,10 @@
     "Grpc.Core.Tests.ClientServerTest",
     "Grpc.Core.Tests.CompressionTest",
     "Grpc.Core.Tests.ContextPropagationTest",
+    "Grpc.Core.Tests.ContextualMarshallerTest",
     "Grpc.Core.Tests.GrpcEnvironmentTest",
     "Grpc.Core.Tests.HalfcloseTest",
+    "Grpc.Core.Tests.MarshallerTest",
     "Grpc.Core.Tests.MarshallingErrorsTest",
     "Grpc.Core.Tests.MetadataTest",
     "Grpc.Core.Tests.PerformanceTest",