From a45cd8eadb87f7529105fb69dc61505984fab7da Mon Sep 17 00:00:00 2001 From: Steve Suh Date: Fri, 4 Sep 2020 18:58:52 -0700 Subject: [PATCH 1/7] initial commit --- src/csharp/Microsoft.Spark/Sql/DataFrame.cs | 42 +++++++++- .../Microsoft.Spark/Sql/RowCollector.cs | 77 +++++++++++++++++++ 2 files changed, 118 insertions(+), 1 deletion(-) diff --git a/src/csharp/Microsoft.Spark/Sql/DataFrame.cs b/src/csharp/Microsoft.Spark/Sql/DataFrame.cs index ed2052143..cedfac141 100644 --- a/src/csharp/Microsoft.Spark/Sql/DataFrame.cs +++ b/src/csharp/Microsoft.Spark/Sql/DataFrame.cs @@ -722,9 +722,28 @@ public IEnumerable Collect() /// Row objects public IEnumerable ToLocalIterator() { - return GetRows("toPythonIterator"); + Version version = SparkEnvironment.SparkVersion; + return version.Major switch + { + 2 => GetRows("toPythonIterator"), + 3 => ToLocalIterator(false), + _ => throw new NotSupportedException($"Spark {version} not supported.") + }; } + /// + /// Returns an iterator that contains all of the rows in this `DataFrame`. + /// The iterator will consume as much memory as the largest partition in this `DataFrame`. + /// With prefetch it may consume up to the memory of the 2 largest partitions. + /// + /// + /// If Spark should pre-fetch the next partition before it is needed. + /// + /// Row objects + [Since(Versions.V3_0_0)] + public IEnumerable ToLocalIterator(bool prefetchPartitions) => + GetRowsV3_0_0(_jvmObject.Invoke("toPythonIterator", prefetchPartitions)); + /// /// Returns the number of rows in the `DataFrame`. /// @@ -917,6 +936,27 @@ private IEnumerable GetRows(string funcName) } } + /// + /// Returns row objects based on the info returned from calling + /// + /// + /// The object to extract the connection info from. + /// + private IEnumerable GetRowsV3_0_0(object info) + { + var infos = (JvmObjectReference[])info; + var port = (int)infos[0].Invoke("intValue"); + var secret = (string)infos[1].Invoke("toString"); + JvmObjectReference server = infos[2]; + + using ISocketWrapper socket = SocketFactory.CreateSocket(); + socket.Connect(IPAddress.Loopback, port, secret); + foreach (Row row in new RowCollector().SynchronousCollect(socket, server)) + { + yield return row; + } + } + /// /// Returns a tuple of port number and secret string which are /// used for connecting with Spark to receive rows for this `DataFrame`. diff --git a/src/csharp/Microsoft.Spark/Sql/RowCollector.cs b/src/csharp/Microsoft.Spark/Sql/RowCollector.cs index f48545ea6..fa67aaf96 100644 --- a/src/csharp/Microsoft.Spark/Sql/RowCollector.cs +++ b/src/csharp/Microsoft.Spark/Sql/RowCollector.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Collections; using System.Collections.Generic; using System.IO; using Microsoft.Spark.Interop.Ipc; @@ -37,5 +38,81 @@ public IEnumerable Collect(ISocketWrapper socket) } } } + + /// + /// Synchronously collects pickled row objects from the given socket. + /// + /// Socket the get the stream from + /// The jvm socket auth server + /// Collection of row objects + public IEnumerable SynchronousCollect(ISocketWrapper socket, JvmObjectReference server) => + new SynchronousRowCollector(socket, server); + + /// + /// SynchronousRowCollector synchronously collects Row objects from a socket. + /// + private class SynchronousRowCollector : IEnumerable + { + private readonly ISocketWrapper _socket; + private readonly Stream _inputStream; + private readonly Stream _outputStream; + private readonly JvmObjectReference _server; + + private int _readStatus = 1; + private IEnumerable _collectEnumerable = null; + + internal SynchronousRowCollector(ISocketWrapper socket, JvmObjectReference server) + { + _socket = socket; + _inputStream = socket.InputStream; + _outputStream = socket.OutputStream; + _server = server; + } + + ~SynchronousRowCollector() + { + // If iterator is not fully consumed + if ((_readStatus == 1) && (_collectEnumerable != null)) + { + // Finish consuming partition data stream + foreach (Row _ in _collectEnumerable) + { + } + + // Tell Java to stop sending data and close connection + SerDe.Write(_outputStream, 0); + _outputStream.Flush(); + } + } + + public IEnumerator GetEnumerator() + { + while (_readStatus == 1) + { + // Request next partition data from Java + SerDe.Write(_outputStream, 1); + _outputStream.Flush(); + + // If response is 1 then there is a partition to read, if 0 then fully consumed + _readStatus = SerDe.ReadInt32(_inputStream); + if (_readStatus == 1) + { + // Load the partition data from stream and read each item + _collectEnumerable = new RowCollector().Collect(_socket); + foreach (Row row in _collectEnumerable) + { + yield return row; + } + } + else if (_readStatus == -1) + { + // An error occurred, join serving thread and raise any exceptions from the JVM + _server.Invoke("getResult"); + } + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } } } From e875adeb7d216472ff8732441b976e624b9322b0 Mon Sep 17 00:00:00 2001 From: Steve Suh Date: Fri, 4 Sep 2020 20:18:55 -0700 Subject: [PATCH 2/7] add returns description. --- src/csharp/Microsoft.Spark/Sql/DataFrame.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/csharp/Microsoft.Spark/Sql/DataFrame.cs b/src/csharp/Microsoft.Spark/Sql/DataFrame.cs index cedfac141..ce65fe945 100644 --- a/src/csharp/Microsoft.Spark/Sql/DataFrame.cs +++ b/src/csharp/Microsoft.Spark/Sql/DataFrame.cs @@ -922,7 +922,7 @@ public DataStreamWriter WriteStream() => /// "collectToPython"). /// /// - /// + /// objects private IEnumerable GetRows(string funcName) { (int port, string secret) = GetConnectionInfo(funcName); @@ -941,7 +941,7 @@ private IEnumerable GetRows(string funcName) /// /// /// The object to extract the connection info from. - /// + /// objects private IEnumerable GetRowsV3_0_0(object info) { var infos = (JvmObjectReference[])info; From c62f114d289901ff01b082b23e3b55b39dfc90ea Mon Sep 17 00:00:00 2001 From: Steve Suh Date: Fri, 4 Sep 2020 20:24:24 -0700 Subject: [PATCH 3/7] Add test --- .../IpcTests/Sql/DataFrameTests.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/csharp/Microsoft.Spark.E2ETest/IpcTests/Sql/DataFrameTests.cs b/src/csharp/Microsoft.Spark.E2ETest/IpcTests/Sql/DataFrameTests.cs index 4fa9904e3..c4dfbc121 100644 --- a/src/csharp/Microsoft.Spark.E2ETest/IpcTests/Sql/DataFrameTests.cs +++ b/src/csharp/Microsoft.Spark.E2ETest/IpcTests/Sql/DataFrameTests.cs @@ -691,5 +691,14 @@ public void TestSignaturesV2_4_X() Assert.IsType(df.Pivot(Col("age"), values)); } } + + /// + /// Test signatures for APIs introduced in Spark 3.* + /// + [SkipIfSparkVersionIsLessThan(Versions.V3_0_0)] + public void TestSignaturesV3_X_X() + { + Assert.IsType(_df.ToLocalIterator(true).ToArray()); + } } } From 84c36090daef128eac9eb5117d1aefda2188f763 Mon Sep 17 00:00:00 2001 From: Steve Suh Date: Fri, 4 Sep 2020 23:29:09 -0700 Subject: [PATCH 4/7] cleanup --- src/csharp/Microsoft.Spark/Sql/DataFrame.cs | 38 ++++++++------------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/src/csharp/Microsoft.Spark/Sql/DataFrame.cs b/src/csharp/Microsoft.Spark/Sql/DataFrame.cs index ce65fe945..fac2bb293 100644 --- a/src/csharp/Microsoft.Spark/Sql/DataFrame.cs +++ b/src/csharp/Microsoft.Spark/Sql/DataFrame.cs @@ -741,8 +741,21 @@ public IEnumerable ToLocalIterator() /// /// Row objects [Since(Versions.V3_0_0)] - public IEnumerable ToLocalIterator(bool prefetchPartitions) => - GetRowsV3_0_0(_jvmObject.Invoke("toPythonIterator", prefetchPartitions)); + public IEnumerable ToLocalIterator(bool prefetchPartitions) + { + var info = + (JvmObjectReference[])_jvmObject.Invoke("toPythonIterator", prefetchPartitions); + var port = (int)info[0].Invoke("intValue"); + var secret = (string)info[1].Invoke("toString"); + JvmObjectReference server = info[2]; + + using ISocketWrapper socket = SocketFactory.CreateSocket(); + socket.Connect(IPAddress.Loopback, port, secret); + foreach (Row row in new RowCollector().SynchronousCollect(socket, server)) + { + yield return row; + } + } /// /// Returns the number of rows in the `DataFrame`. @@ -936,27 +949,6 @@ private IEnumerable GetRows(string funcName) } } - /// - /// Returns row objects based on the info returned from calling - /// - /// - /// The object to extract the connection info from. - /// objects - private IEnumerable GetRowsV3_0_0(object info) - { - var infos = (JvmObjectReference[])info; - var port = (int)infos[0].Invoke("intValue"); - var secret = (string)infos[1].Invoke("toString"); - JvmObjectReference server = infos[2]; - - using ISocketWrapper socket = SocketFactory.CreateSocket(); - socket.Connect(IPAddress.Loopback, port, secret); - foreach (Row row in new RowCollector().SynchronousCollect(socket, server)) - { - yield return row; - } - } - /// /// Returns a tuple of port number and secret string which are /// used for connecting with Spark to receive rows for this `DataFrame`. From d5d4a33aa2ac94bc3374587f816572e4bdb38eb7 Mon Sep 17 00:00:00 2001 From: Steve Suh Date: Fri, 4 Sep 2020 23:42:04 -0700 Subject: [PATCH 5/7] clean up. --- src/csharp/Microsoft.Spark/Sql/RowCollector.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/csharp/Microsoft.Spark/Sql/RowCollector.cs b/src/csharp/Microsoft.Spark/Sql/RowCollector.cs index fa67aaf96..f08a3d2fb 100644 --- a/src/csharp/Microsoft.Spark/Sql/RowCollector.cs +++ b/src/csharp/Microsoft.Spark/Sql/RowCollector.cs @@ -54,8 +54,6 @@ public IEnumerable SynchronousCollect(ISocketWrapper socket, JvmObjectRefer private class SynchronousRowCollector : IEnumerable { private readonly ISocketWrapper _socket; - private readonly Stream _inputStream; - private readonly Stream _outputStream; private readonly JvmObjectReference _server; private int _readStatus = 1; @@ -64,8 +62,6 @@ private class SynchronousRowCollector : IEnumerable internal SynchronousRowCollector(ISocketWrapper socket, JvmObjectReference server) { _socket = socket; - _inputStream = socket.InputStream; - _outputStream = socket.OutputStream; _server = server; } @@ -80,21 +76,25 @@ internal SynchronousRowCollector(ISocketWrapper socket, JvmObjectReference serve } // Tell Java to stop sending data and close connection - SerDe.Write(_outputStream, 0); - _outputStream.Flush(); + Stream outputStream = _socket.OutputStream; + SerDe.Write(outputStream, 0); + outputStream.Flush(); } } public IEnumerator GetEnumerator() { + Stream inputStream = _socket.InputStream; + Stream outputStream = _socket.OutputStream; + while (_readStatus == 1) { // Request next partition data from Java - SerDe.Write(_outputStream, 1); - _outputStream.Flush(); + SerDe.Write(outputStream, 1); + outputStream.Flush(); // If response is 1 then there is a partition to read, if 0 then fully consumed - _readStatus = SerDe.ReadInt32(_inputStream); + _readStatus = SerDe.ReadInt32(inputStream); if (_readStatus == 1) { // Load the partition data from stream and read each item From 8a8660547c51f046c1254f24717d1a7eea48bd3b Mon Sep 17 00:00:00 2001 From: Steve Suh Date: Sat, 5 Sep 2020 17:49:36 -0700 Subject: [PATCH 6/7] PR comment. --- .../IpcTests/Sql/DataFrameTests.cs | 16 +++- src/csharp/Microsoft.Spark/Sql/DataFrame.cs | 48 ++++++------ .../Microsoft.Spark/Sql/RowCollector.cs | 76 ++++++++++++------- 3 files changed, 88 insertions(+), 52 deletions(-) diff --git a/src/csharp/Microsoft.Spark.E2ETest/IpcTests/Sql/DataFrameTests.cs b/src/csharp/Microsoft.Spark.E2ETest/IpcTests/Sql/DataFrameTests.cs index c4dfbc121..f036ad346 100644 --- a/src/csharp/Microsoft.Spark.E2ETest/IpcTests/Sql/DataFrameTests.cs +++ b/src/csharp/Microsoft.Spark.E2ETest/IpcTests/Sql/DataFrameTests.cs @@ -698,7 +698,21 @@ public void TestSignaturesV2_4_X() [SkipIfSparkVersionIsLessThan(Versions.V3_0_0)] public void TestSignaturesV3_X_X() { - Assert.IsType(_df.ToLocalIterator(true).ToArray()); + // Validate ToLocalIterator + var data = new List + { + new GenericRow(new object[] { "Alice", 20}), + new GenericRow(new object[] { "Bob", 30}) + }; + var schema = new StructType(new List() + { + new StructField("Name", new StringType()), + new StructField("Age", new IntegerType()) + }); + DataFrame df = _spark.CreateDataFrame(data, schema); + IEnumerable actual = df.ToLocalIterator(true).ToArray(); + IEnumerable expected = data.Select(r => new Row(r.Values, schema)); + Assert.Equal(expected, actual); } } } diff --git a/src/csharp/Microsoft.Spark/Sql/DataFrame.cs b/src/csharp/Microsoft.Spark/Sql/DataFrame.cs index fac2bb293..b695e4c07 100644 --- a/src/csharp/Microsoft.Spark/Sql/DataFrame.cs +++ b/src/csharp/Microsoft.Spark/Sql/DataFrame.cs @@ -743,15 +743,13 @@ public IEnumerable ToLocalIterator() [Since(Versions.V3_0_0)] public IEnumerable ToLocalIterator(bool prefetchPartitions) { - var info = - (JvmObjectReference[])_jvmObject.Invoke("toPythonIterator", prefetchPartitions); - var port = (int)info[0].Invoke("intValue"); - var secret = (string)info[1].Invoke("toString"); - JvmObjectReference server = info[2]; - + (int port, string secret, JvmObjectReference server) = + ParseConnectionInfo( + _jvmObject.Invoke("toPythonIterator", prefetchPartitions), + true); using ISocketWrapper socket = SocketFactory.CreateSocket(); socket.Connect(IPAddress.Loopback, port, secret); - foreach (Row row in new RowCollector().SynchronousCollect(socket, server)) + foreach (Row row in new RowCollector().Collect(socket, server)) { yield return row; } @@ -934,18 +932,18 @@ public DataStreamWriter WriteStream() => /// Returns row objects based on the function (either "toPythonIterator" or /// "collectToPython"). /// - /// + /// + /// The name of the function to call, either "toPythonIterator" or "collectToPython". + /// /// objects private IEnumerable GetRows(string funcName) { - (int port, string secret) = GetConnectionInfo(funcName); - using (ISocketWrapper socket = SocketFactory.CreateSocket()) + (int port, string secret, JvmObjectReference _) = GetConnectionInfo(funcName); + using ISocketWrapper socket = SocketFactory.CreateSocket(); + socket.Connect(IPAddress.Loopback, port, secret); + foreach (Row row in new RowCollector().Collect(socket)) { - socket.Connect(IPAddress.Loopback, port, secret); - foreach (Row row in new RowCollector().Collect(socket)) - { - yield return row; - } + yield return row; } } @@ -954,28 +952,32 @@ private IEnumerable GetRows(string funcName) /// used for connecting with Spark to receive rows for this `DataFrame`. /// /// A tuple of port number and secret string - private (int, string) GetConnectionInfo(string funcName) + private (int, string, JvmObjectReference) GetConnectionInfo(string funcName) { object result = _jvmObject.Invoke(funcName); Version version = SparkEnvironment.SparkVersion; return (version.Major, version.Minor, version.Build) switch { // In spark 2.3.0, PythonFunction.serveIterator() returns a port number. - (2, 3, 0) => ((int)result, string.Empty), + (2, 3, 0) => ((int)result, string.Empty, null), // From spark >= 2.3.1, PythonFunction.serveIterator() returns a pair // where the first is a port number and the second is the secret // string to use for the authentication. - (2, 3, _) => ParseConnectionInfo(result), - (2, 4, _) => ParseConnectionInfo(result), - (3, 0, _) => ParseConnectionInfo(result), + (2, 3, _) => ParseConnectionInfo(result, false), + (2, 4, _) => ParseConnectionInfo(result, false), + (3, 0, _) => ParseConnectionInfo(result, false), _ => throw new NotSupportedException($"Spark {version} not supported.") }; } - private (int, string) ParseConnectionInfo(object info) + private (int, string, JvmObjectReference) ParseConnectionInfo( + object info, + bool parseServer) { - var pair = (JvmObjectReference[])info; - return ((int)pair[0].Invoke("intValue"), (string)pair[1].Invoke("toString")); + var infos = (JvmObjectReference[])info; + return ((int)infos[0].Invoke("intValue"), + (string)infos[1].Invoke("toString"), + parseServer ? infos[2] : null); } private DataFrame WrapAsDataFrame(object obj) => new DataFrame((JvmObjectReference)obj); diff --git a/src/csharp/Microsoft.Spark/Sql/RowCollector.cs b/src/csharp/Microsoft.Spark/Sql/RowCollector.cs index f08a3d2fb..4d15ae902 100644 --- a/src/csharp/Microsoft.Spark/Sql/RowCollector.cs +++ b/src/csharp/Microsoft.Spark/Sql/RowCollector.cs @@ -4,6 +4,7 @@ using System.Collections; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using Microsoft.Spark.Interop.Ipc; using Microsoft.Spark.Network; @@ -19,8 +20,8 @@ internal sealed class RowCollector /// /// Collects pickled row objects from the given socket. /// - /// Socket the get the stream from - /// Collection of row objects + /// Socket the get the stream from. + /// Collection of row objects. public IEnumerable Collect(ISocketWrapper socket) { Stream inputStream = socket.InputStream; @@ -40,45 +41,58 @@ public IEnumerable Collect(ISocketWrapper socket) } /// - /// Synchronously collects pickled row objects from the given socket. + /// Collects pickled row objects from the given socket. Collects rows in partitions + /// by leveraging . /// - /// Socket the get the stream from - /// The jvm socket auth server - /// Collection of row objects - public IEnumerable SynchronousCollect(ISocketWrapper socket, JvmObjectReference server) => - new SynchronousRowCollector(socket, server); + /// Socket the get the stream from. + /// The JVM socket auth server. + /// Collection of row objects. + public IEnumerable Collect(ISocketWrapper socket, JvmObjectReference server) => + new LocalIteratorFromSocket(socket, server); /// - /// SynchronousRowCollector synchronously collects Row objects from a socket. + /// LocalIteratorFromSocket creates a synchronous local iterable over + /// a socket. + /// + /// Note that the implementation mirrors _local_iterator_from_socket in + /// PySpark: spark/python/pyspark/rdd.py /// - private class SynchronousRowCollector : IEnumerable + private class LocalIteratorFromSocket : IEnumerable { private readonly ISocketWrapper _socket; private readonly JvmObjectReference _server; private int _readStatus = 1; - private IEnumerable _collectEnumerable = null; + private IEnumerable _currentPartitionRows = null; - internal SynchronousRowCollector(ISocketWrapper socket, JvmObjectReference server) + internal LocalIteratorFromSocket(ISocketWrapper socket, JvmObjectReference server) { _socket = socket; _server = server; } - ~SynchronousRowCollector() + ~LocalIteratorFromSocket() { - // If iterator is not fully consumed - if ((_readStatus == 1) && (_collectEnumerable != null)) + // If iterator is not fully consumed. + if ((_readStatus == 1) && (_currentPartitionRows != null)) { - // Finish consuming partition data stream - foreach (Row _ in _collectEnumerable) + try { - } + // Finish consuming partition data stream. + foreach (Row _ in _currentPartitionRows) + { + } - // Tell Java to stop sending data and close connection - Stream outputStream = _socket.OutputStream; - SerDe.Write(outputStream, 0); - outputStream.Flush(); + // Tell Java to stop sending data and close connection. + Stream outputStream = _socket.OutputStream; + SerDe.Write(outputStream, 0); + outputStream.Flush(); + } + catch + { + // Ignore any errors, socket may be automatically closed + // when garbage-collected. + } } } @@ -89,26 +103,32 @@ public IEnumerator GetEnumerator() while (_readStatus == 1) { - // Request next partition data from Java + // Request next partition data from Java. SerDe.Write(outputStream, 1); outputStream.Flush(); - // If response is 1 then there is a partition to read, if 0 then fully consumed + // If response is 1 then there is a partition to read, if 0 then + // fully consumed. _readStatus = SerDe.ReadInt32(inputStream); if (_readStatus == 1) { - // Load the partition data from stream and read each item - _collectEnumerable = new RowCollector().Collect(_socket); - foreach (Row row in _collectEnumerable) + // Load the partition data from stream and read each item. + _currentPartitionRows = new RowCollector().Collect(_socket); + foreach (Row row in _currentPartitionRows) { yield return row; } } else if (_readStatus == -1) { - // An error occurred, join serving thread and raise any exceptions from the JVM + // An error occurred, join serving thread and raise any exceptions + // from the JVM. _server.Invoke("getResult"); } + else + { + Debug.Assert(_readStatus == 0); + } } } From 5cd4564601bca4013560209be1f44890c695f4b5 Mon Sep 17 00:00:00 2001 From: Steve Suh Date: Sat, 5 Sep 2020 22:34:30 -0700 Subject: [PATCH 7/7] PR comments. --- src/csharp/Microsoft.Spark/Sql/DataFrame.cs | 4 ++-- src/csharp/Microsoft.Spark/Sql/RowCollector.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/csharp/Microsoft.Spark/Sql/DataFrame.cs b/src/csharp/Microsoft.Spark/Sql/DataFrame.cs index b695e4c07..1c4d1de8d 100644 --- a/src/csharp/Microsoft.Spark/Sql/DataFrame.cs +++ b/src/csharp/Microsoft.Spark/Sql/DataFrame.cs @@ -938,7 +938,7 @@ public DataStreamWriter WriteStream() => /// objects private IEnumerable GetRows(string funcName) { - (int port, string secret, JvmObjectReference _) = GetConnectionInfo(funcName); + (int port, string secret, _) = GetConnectionInfo(funcName); using ISocketWrapper socket = SocketFactory.CreateSocket(); socket.Connect(IPAddress.Loopback, port, secret); foreach (Row row in new RowCollector().Collect(socket)) @@ -951,7 +951,7 @@ private IEnumerable GetRows(string funcName) /// Returns a tuple of port number and secret string which are /// used for connecting with Spark to receive rows for this `DataFrame`. /// - /// A tuple of port number and secret string + /// A tuple of port number, secret string, and JVM socket auth server. private (int, string, JvmObjectReference) GetConnectionInfo(string funcName) { object result = _jvmObject.Invoke(funcName); diff --git a/src/csharp/Microsoft.Spark/Sql/RowCollector.cs b/src/csharp/Microsoft.Spark/Sql/RowCollector.cs index 4d15ae902..431a5ea3c 100644 --- a/src/csharp/Microsoft.Spark/Sql/RowCollector.cs +++ b/src/csharp/Microsoft.Spark/Sql/RowCollector.cs @@ -121,8 +121,8 @@ public IEnumerator GetEnumerator() } else if (_readStatus == -1) { - // An error occurred, join serving thread and raise any exceptions - // from the JVM. + // An error occurred, join serving thread and raise any exceptions from + // the JVM. The exception stack trace will appear in the driver logs. _server.Invoke("getResult"); } else