From 26d3e774df8d601c5ce9c5fb181f846b1fe07049 Mon Sep 17 00:00:00 2001 From: Jan Tattermusch Date: Thu, 16 Aug 2018 13:13:30 +0200 Subject: new C# serialization API --- src/csharp/Grpc.Core/DeserializationContext.cs | 49 +++++++++++ src/csharp/Grpc.Core/Marshaller.cs | 110 ++++++++++++++++++++++--- src/csharp/Grpc.Core/SerializationContext.cs | 34 ++++++++ 3 files changed, 181 insertions(+), 12 deletions(-) create mode 100644 src/csharp/Grpc.Core/DeserializationContext.cs create mode 100644 src/csharp/Grpc.Core/SerializationContext.cs (limited to 'src/csharp') diff --git a/src/csharp/Grpc.Core/DeserializationContext.cs b/src/csharp/Grpc.Core/DeserializationContext.cs new file mode 100644 index 0000000000..17f0ba2805 --- /dev/null +++ b/src/csharp/Grpc.Core/DeserializationContext.cs @@ -0,0 +1,49 @@ +#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 +{ + /// + /// Provides access to the payload being deserialized when deserializing messages. + /// + public abstract class DeserializationContext + { + /// + /// Returns true if there is a payload to deserialize (= payload is not null), false otherwise. + /// + public virtual bool HasPayload => PayloadLength.HasValue; + + /// + /// Get the total length of the payload in bytes or null if the payload is null. + /// + public abstract int? PayloadLength { get; } + + /// + /// 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). + /// + /// byte array containing the entire payload or null if there is no payload. + public abstract byte[] PayloadAsNewBuffer(); + } +} diff --git a/src/csharp/Grpc.Core/Marshaller.cs b/src/csharp/Grpc.Core/Marshaller.cs index 1d758ca935..df59cbd8d3 100644 --- a/src/csharp/Grpc.Core/Marshaller.cs +++ b/src/csharp/Grpc.Core/Marshaller.cs @@ -29,36 +29,122 @@ namespace Grpc.Core readonly Func serializer; readonly Func deserializer; + readonly Action contextualSerializer; + readonly Func contextualDeserializer; + /// - /// Initializes a new marshaller. + /// Initializes a new marshaller from simple serialize/deserialize functions. /// /// Function that will be used to serialize messages. /// Function that will be used to deserialize messages. public Marshaller(Func serializer, Func 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; } /// - /// 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. /// - public Func Serializer + /// Function that will be used to serialize messages. + /// Function that will be used to deserialize messages. + public Marshaller(Action serializer, Func deserializer) { - get - { - return this.serializer; - } + this.contextualSerializer = GrpcPreconditions.CheckNotNull(serializer, nameof(serializer)); + this.contextualDeserializer = GrpcPreconditions.CheckNotNull(deserializer, nameof(deserializer)); + this.serializer = EmulateSimpleSerializer; + this.deserializer = EmulateSimpleDeserializer; } + /// + /// Gets the serializer function. + /// + public Func Serializer => this.serializer; + /// /// Gets the deserializer function. /// - public Func Deserializer + public Func Deserializer => this.deserializer; + + /// + /// Gets the serializer function. + /// Note: experimental API that can change or be removed without any prior notice. + /// + public Action ContextualSerializer => this.contextualSerializer; + + /// + /// Gets the serializer function. + /// Note: experimental API that can change or be removed without any prior notice. + /// + public Func 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 + 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 + 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; + + public EmulatedDeserializationContext(byte[] payload) + { + this.payload = payload; + } + + public override int? PayloadLength => payload?.Length; + + public override byte[] PayloadAsNewBuffer() { - return this.deserializer; + return payload; } } } diff --git a/src/csharp/Grpc.Core/SerializationContext.cs b/src/csharp/Grpc.Core/SerializationContext.cs new file mode 100644 index 0000000000..cf4d1595da --- /dev/null +++ b/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 +{ + /// + /// Provides storage for payload when serializing a message. + /// + public abstract class SerializationContext + { + /// + /// 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. + /// + /// the serialized form of current message + public abstract void Complete(byte[] payload); + } +} -- cgit v1.2.3 From fb704ee949047ebc1d78f00b4b7d6938f8e89a6a Mon Sep 17 00:00:00 2001 From: Jan Tattermusch Date: Mon, 27 Aug 2018 20:39:26 +0200 Subject: deserialization context always has non-null payload --- src/csharp/Grpc.Core/DeserializationContext.cs | 11 +++-------- src/csharp/Grpc.Core/Marshaller.cs | 4 ++-- 2 files changed, 5 insertions(+), 10 deletions(-) (limited to 'src/csharp') diff --git a/src/csharp/Grpc.Core/DeserializationContext.cs b/src/csharp/Grpc.Core/DeserializationContext.cs index 17f0ba2805..96de08f76d 100644 --- a/src/csharp/Grpc.Core/DeserializationContext.cs +++ b/src/csharp/Grpc.Core/DeserializationContext.cs @@ -24,14 +24,9 @@ namespace Grpc.Core public abstract class DeserializationContext { /// - /// Returns true if there is a payload to deserialize (= payload is not null), false otherwise. + /// Get the total length of the payload in bytes. /// - public virtual bool HasPayload => PayloadLength.HasValue; - - /// - /// Get the total length of the payload in bytes or null if the payload is null. - /// - public abstract int? PayloadLength { get; } + public abstract int PayloadLength { get; } /// /// Gets the entire payload as a newly allocated byte array. @@ -43,7 +38,7 @@ namespace Grpc.Core /// 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). /// - /// byte array containing the entire payload or null if there is no payload. + /// byte array containing the entire payload. public abstract byte[] PayloadAsNewBuffer(); } } diff --git a/src/csharp/Grpc.Core/Marshaller.cs b/src/csharp/Grpc.Core/Marshaller.cs index df59cbd8d3..ad01b9383c 100644 --- a/src/csharp/Grpc.Core/Marshaller.cs +++ b/src/csharp/Grpc.Core/Marshaller.cs @@ -137,10 +137,10 @@ namespace Grpc.Core public EmulatedDeserializationContext(byte[] payload) { - this.payload = payload; + this.payload = GrpcPreconditions.CheckNotNull(payload); } - public override int? PayloadLength => payload?.Length; + public override int PayloadLength => payload.Length; public override byte[] PayloadAsNewBuffer() { -- cgit v1.2.3 From 6ba637f7ecdd45bf43c1a7959115190ca5a2f5c8 Mon Sep 17 00:00:00 2001 From: Jan Tattermusch Date: Mon, 27 Aug 2018 20:42:06 +0200 Subject: add Marshallers.Create factory method --- src/csharp/Grpc.Core/Marshaller.cs | 9 +++++++++ 1 file changed, 9 insertions(+) (limited to 'src/csharp') diff --git a/src/csharp/Grpc.Core/Marshaller.cs b/src/csharp/Grpc.Core/Marshaller.cs index ad01b9383c..7f010dc419 100644 --- a/src/csharp/Grpc.Core/Marshaller.cs +++ b/src/csharp/Grpc.Core/Marshaller.cs @@ -162,6 +162,15 @@ namespace Grpc.Core return new Marshaller(serializer, deserializer); } + /// + /// 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. + /// + public static Marshaller Create(Action serializer, Func deserializer) + { + return new Marshaller(serializer, deserializer); + } + /// /// Returns a marshaller for string type. This is useful for testing. /// -- cgit v1.2.3 From 63a31d85f1557f1c0e2b1be04e7bd4d07a88607e Mon Sep 17 00:00:00 2001 From: Jan Tattermusch Date: Sun, 23 Sep 2018 18:58:37 -0700 Subject: contextual marshaller test --- .../Grpc.Core.Tests/ContextualMarshallerTest.cs | 119 +++++++++++++++++++++ src/csharp/tests.json | 1 + 2 files changed, 120 insertions(+) create mode 100644 src/csharp/Grpc.Core.Tests/ContextualMarshallerTest.cs (limited to 'src/csharp') diff --git a/src/csharp/Grpc.Core.Tests/ContextualMarshallerTest.cs b/src/csharp/Grpc.Core.Tests/ContextualMarshallerTest.cs new file mode 100644 index 0000000000..c3aee726f2 --- /dev/null +++ b/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( + (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((request, context) => + { + return Task.FromResult(request); + }); + Assert.AreEqual("ABC", Calls.BlockingUnaryCall(helper.CreateUnaryCall(), "ABC")); + } + + [Test] + public void ResponseParsingError_UnaryResponse() + { + helper.UnaryHandler = new UnaryServerMethod((request, context) => + { + return Task.FromResult("UNPARSEABLE_VALUE"); + }); + + var ex = Assert.Throws(() => Calls.BlockingUnaryCall(helper.CreateUnaryCall(), "REQUEST")); + Assert.AreEqual(StatusCode.Internal, ex.Status.StatusCode); + } + + [Test] + public void RequestSerializationError_BlockingUnary() + { + Assert.Throws(() => Calls.BlockingUnaryCall(helper.CreateUnaryCall(), "UNSERIALIZABLE_VALUE")); + } + + [Test] + public void SerializationResultIsNull_BlockingUnary() + { + Assert.Throws(() => Calls.BlockingUnaryCall(helper.CreateUnaryCall(), "SERIALIZE_TO_NULL")); + } + } +} diff --git a/src/csharp/tests.json b/src/csharp/tests.json index c2f243fe0a..e4feb4dc3a 100644 --- a/src/csharp/tests.json +++ b/src/csharp/tests.json @@ -23,6 +23,7 @@ "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.MarshallingErrorsTest", -- cgit v1.2.3 From a2a4629614bb79e3a4d7ea6594e31e33d63a65be Mon Sep 17 00:00:00 2001 From: Jan Tattermusch Date: Sun, 23 Sep 2018 21:10:03 -0700 Subject: add MarshallerTest --- src/csharp/Grpc.Core.Tests/MarshallerTest.cs | 105 +++++++++++++++++++++++++++ src/csharp/tests.json | 1 + 2 files changed, 106 insertions(+) create mode 100644 src/csharp/Grpc.Core.Tests/MarshallerTest.cs (limited to 'src/csharp') diff --git a/src/csharp/Grpc.Core.Tests/MarshallerTest.cs b/src/csharp/Grpc.Core.Tests/MarshallerTest.cs new file mode 100644 index 0000000000..97f64a0575 --- /dev/null +++ b/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 simpleSerializer = System.Text.Encoding.UTF8.GetBytes; + Func simpleDeserializer = System.Text.Encoding.UTF8.GetString; + var marshaller = new Marshaller(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 contextualSerializer = (str, context) => + { + var bytes = System.Text.Encoding.UTF8.GetBytes(str); + context.Complete(bytes); + }; + Func contextualDeserializer = (context) => + { + return System.Text.Encoding.UTF8.GetString(context.PayloadAsNewBuffer()); + }; + var marshaller = new Marshaller(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; + } + } + } +} diff --git a/src/csharp/tests.json b/src/csharp/tests.json index e4feb4dc3a..5683d164c6 100644 --- a/src/csharp/tests.json +++ b/src/csharp/tests.json @@ -26,6 +26,7 @@ "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", -- cgit v1.2.3 From 10447318588a281d737af159a5dceec142135a43 Mon Sep 17 00:00:00 2001 From: Jan Tattermusch Date: Tue, 25 Sep 2018 20:37:02 +0200 Subject: add DeserializationContext implementation note --- src/csharp/Grpc.Core/DeserializationContext.cs | 2 ++ 1 file changed, 2 insertions(+) (limited to 'src/csharp') diff --git a/src/csharp/Grpc.Core/DeserializationContext.cs b/src/csharp/Grpc.Core/DeserializationContext.cs index 96de08f76d..5b6372ef85 100644 --- a/src/csharp/Grpc.Core/DeserializationContext.cs +++ b/src/csharp/Grpc.Core/DeserializationContext.cs @@ -37,6 +37,8 @@ namespace Grpc.Core /// 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 DeserializationContext implementations are free to assume so. /// /// byte array containing the entire payload. public abstract byte[] PayloadAsNewBuffer(); -- cgit v1.2.3 From c2fd689bad731f30b2ab43a5613e164f7e44be5c Mon Sep 17 00:00:00 2001 From: Jan Tattermusch Date: Tue, 25 Sep 2018 23:02:42 +0200 Subject: address comments --- src/csharp/Grpc.Core/Marshaller.cs | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'src/csharp') diff --git a/src/csharp/Grpc.Core/Marshaller.cs b/src/csharp/Grpc.Core/Marshaller.cs index 7f010dc419..0af9aa586b 100644 --- a/src/csharp/Grpc.Core/Marshaller.cs +++ b/src/csharp/Grpc.Core/Marshaller.cs @@ -57,6 +57,8 @@ namespace Grpc.Core { 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; } @@ -87,6 +89,7 @@ namespace Grpc.Core private byte[] EmulateSimpleSerializer(T msg) { // 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(); @@ -96,6 +99,7 @@ namespace Grpc.Core 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); } @@ -134,6 +138,7 @@ namespace Grpc.Core internal class EmulatedDeserializationContext : DeserializationContext { readonly byte[] payload; + bool alreadyCalledPayloadAsNewBuffer; public EmulatedDeserializationContext(byte[] payload) { @@ -144,6 +149,8 @@ namespace Grpc.Core public override byte[] PayloadAsNewBuffer() { + GrpcPreconditions.CheckState(!alreadyCalledPayloadAsNewBuffer); + alreadyCalledPayloadAsNewBuffer = true; return payload; } } -- cgit v1.2.3