1

I am trying to expose an observable via a GRPC stream. my simplified code looks like this:

public override async Task Feed(Request request, IServerStreamWriter<Response> responseStream, ServerCallContext context)
        {
            var result = new Result();
            try
            {
                await Observable.ForEachAsync(async value =>
                {
                    await responseStream.WriteAsync(value);
                });

            }
            catch (Exception ex)
            {
                Log.Info("Session ended:" + ex);
            }
        }

I receive the following error:

W0123 14:30:59.709715 Grpc.Core.Internal.ServerStreamingServerCallHandler2 Exception occured in handler. System.ArgumentException: Der Wert liegt außerhalb des erwarteten Bereichs. bei Grpc.Core.Internal.ServerStreamingServerCallHandler2.d__4.MoveNext() W0123 14:30:59.732716 Grpc.Core.Server Exception while handling RPC. System.InvalidOperationException: Der Vorgang ist aufgrund des aktuellen Zustands des Objekts ungültig. bei Grpc.Core.Internal.AsyncCallServer2.SendStatusFromServerAsync(Status status, Metadata trailers, Tuple2 optionalWrite) bei Grpc.Core.Internal.ServerStreamingServerCallHandler`2.d__4.MoveNext() --- Ende der Stapelüberwachung vom vorhergehenden Ort, an dem die Ausnahme ausgelöst wurde --- bei System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) bei System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) bei Grpc.Core.Server.d__34.MoveNext()

How would you recommend to handle this? I guess I would need to process the ForEachAsync in the same thread.

weismat
  • 7,195
  • 3
  • 43
  • 58
  • I have moved to BufferBlock as the synchronisation mechanism instead of rx. This seems to be cleaner and easier than using rx with scheduling. – weismat Jan 25 '17 at 08:18

3 Answers3

3

With the gRPC streaming API, you are only allowed to write one item at a time. If you start another WriteAsync() operation before the previous one finishes, you'll get an exception. You also need to finish all your writes before returning from the method handler (the Feed method in this case). The reason only one write is allowed at a time is to ensure gRPC's flow control works well.

In your case the Rx API doesn't seem to be capable of ensuring that, so one way to solve this would be to use an intermediate buffer.

Jan Tattermusch
  • 1,515
  • 9
  • 9
  • I agree on this side. The other question is how the lifetime of an IServerStreamWriter is determined as it does not implement IDisposable. As commented I have started to use the BufferBlock as an asynchronous Fifo Queue as a sync me – weismat Jan 26 '17 at 08:19
  • @weismat: Were you able to create an observable from gRPC stream? I don't understand how BufferBlock helped you to achieve this. Could you give an example? Thx. – Sven Bardos Aug 02 '17 at 06:50
  • a bit related https://stackoverflow.com/questions/44859534/grpc-async-response-stream-c-sharp – Jan Tattermusch Aug 04 '17 at 07:43
0

How about this?

public override async Task Feed(Request request, IServerStreamWriter<Response> responseStream, ServerCallContext context)
{
    var result = new Result();
    try
    {
        await Observable.Scan(Task.CompletedTask, (preceding,value) =>
            preceding.ContinueWith(_ => responseStream.WriteAsync(value))
        );
    }
    catch (Exception ex)
    {
        Log.Info("Session ended:" + ex);
    }
}

I haven't tested it yet, but I think it works anyway.

Stephen Rauch
  • 47,830
  • 31
  • 106
  • 135
USK
  • 1
0

There are two issues preventing the OP's method from working as expected both stemming from the responseStream.WriteAsync's expectation that only one write can be pending at a time.

  1. When the observable is subscribed to, each 'onNext' will be processed on the same thread that raised the notification. This means you be able to enforce the one write at a time rule. You can use the ObserveOn method to control how each notification will be scheduled.
var eventLoop = new EventLoopScheduler();
var o = myObservable.ObserveOn(eventLoop);
  1. ForEachAsync doesn't work like it looks like one might think with async lambdas. If you drop the async/await here and instead call WriteAsync like this, then the Write will be executed in the expected context.
 responseStream.WriteAsync<Response>(value).Wait();

Here is a complete example...

public override async Task Feed(Request request, IServerStreamWriter<Response> responseStream, ServerCallContext context)
{
  var o = getMyObservable(request);

  try
  {
      var eventLoop = new EventLoopScheduler(); //just for example, use whatever scheduler makes sense for you.
      await o.ObserveOn(eventLoop).ForEachAsync<Response>(value =>
      {
          responseStream.WriteAsync(value).Wait();
      }, 
      context.CancellationToken);

  }
  catch (TaskCanceledException) { }
  catch (Exception ex)
  {
      Log.Info("Session ended:" + ex);
  }

  Log.Info("Session ended normally");
}
Community
  • 1
  • 1
dk.
  • 937
  • 1
  • 10
  • 12