24

I'm trying to write an extension method for System.Net.WebSocket that will turn it into an IObserver using Reactive Extensions (Rx.NET). You can see the code below:

public static IObserver<T> ToObserver<T>(this WebSocket webSocket, IWebSocketMessageSerializer<T> webSocketMessageSerializer)
{
    // Wrap the web socket in an interface that's a little easier to manage
    var webSocketMessageStream = new WebSocketMessageStream(webSocket);

    // Create the output stream to the client
    return Observer.Create<T>(
        onNext:      async message => await webSocketMessageStream.WriteMessageAsync(webSocketMessageSerializer.SerializeMessage(message)),
        onError:     async error   => await webSocketMessageStream.CloseAsync(WebSocketCloseStatus.InternalServerError, string.Format("{0}: {1}", error.GetType(), error.Message)),
        onCompleted: async ()      => await webSocketMessageStream.CloseAsync(WebSocketCloseStatus.NormalClosure, "Server disconnected")
    );
}

This code works, but I am concerned about the use of async/await inside of the onNext, onError, and onCompleted lambdas. I know that this returns an async void lambda, which is frowned upon (and sometimes causes issues that I've already run into myself).

I've been reading up on the Rx.NET documentation as well as blog posts across the internet and I cannot seem to find the proper way (if there is one) to use an async/await method in an IObserver. Is there a proper way to do this? If not, then how can I work around this problem?

svick
  • 236,525
  • 50
  • 385
  • 514
Kevin Craft
  • 849
  • 8
  • 18
  • Your assumptions are wrong. Actually, you're passing `Func>` (or some variant that takes parameters) to `onNext` etc. The delegates don't have `void` return types. They return tasks. – spender Feb 03 '16 at 17:51
  • I'm not sure I follow you. I can assign an async method to an Action like this: Action test = async () => await DoSomethingAsync(); This compiles just fine and the "test" action is not awaitable. Calling test() returns immediately and before DoSomethingAsync() completes. The method definition for Observer.Create() accepts Actions as params for onNext, onError, and onCompleted. This is why the code I posted above works. However, when someone calls OnNext(x) on my IObserver, it does not wait for the asynchronous part of the method to finish before returning. – Kevin Craft Feb 03 '16 at 18:05
  • Oops. My bad. Looking into it now. – spender Feb 03 '16 at 18:21
  • The general guidance (IMO) is that you dont implement `IObserver` or use `Observer.Create`. Instead you should use the `Subscribe` extension method and pass your lambdas into that. – Lee Campbell Feb 04 '16 at 00:47
  • Why do `async` on an observable anyway? The schedulers allow you to be async without compiler help. – Enigmativity Feb 04 '16 at 11:11
  • I'm definitely not an expert on Rx and admittedly am not very familiar with how the scheduler stuff works. My main concern is that I do not want to block a thread while the asynchronous function (WriteMessageAsync in my example) is running. That would ruin the scalability of my app. Would schedulers allow me to free up threads while the async function is running? I'm going to look into this myself as well. – Kevin Craft Feb 04 '16 at 16:06

1 Answers1

25

Subscribe does not take async methods. So what happens here is you are using a fire-and-forget mechanism from async void. The problem is that onNext messages will no longer be serialized.

Instead of calling an async method inside Subscribe, you should wrap it into the pipeline to allow Rx to wait for it.

It's okay to use fire-and-forget on onError and onCompleted because these are guaranteed to be the last thing called from the Observable. Do keep in mind that resources associated with the Observable can be cleaned up after onError and onCompleted returned, before they completed.

I wrote a small extension method that does all this:

    public static IDisposable SubscribeAsync<T>(this IObservable<T> source, Func<T, Task> onNext, Action<Exception> onError, Action onCompleted)
    {
        return source.Select(e => Observable.Defer(() => onNext(e).ToObservable())).Concat()
            .Subscribe(
            e => { }, // empty
            onError,
            onCompleted);
    }

First we convert the async onNext method into an Observable, bridging the two async worlds. This means async/await inside onNext will be respected. Next we wrap the method into Defer so it wont start right when it is created. Instead Concat will call the next defered observable only when the previous one finished.

Ps. I have hope the next release of Rx.Net will have async support in subscribe.

Dorus
  • 7,276
  • 1
  • 30
  • 36
  • What do you mean by "onNext messages will no longer be serialized"? – Asad Saeeduddin Feb 04 '16 at 00:56
  • 2
    What Dorus is saying is that two layers of asynchrony are being introduced here. If one message is received, it will invoke the `WriteMessageAsync` method. If another message is received, then it will execute the `WriteMessageAsync` regardless of if the previous call has completed. This means you could have out of order messages (especially if the TaskPool scheduling gets involved too). i.e. onNext messages will no longer be serialized. – Lee Campbell Feb 04 '16 at 01:31
  • @Dorus, Thanks for the response. Would you mind editing your post to explain what your extension method does in a little more detail? One thing I'm wondering about is if this will actually take advantage of async/await or will it block a thread while waiting for OnNext to complete? Also, I noticed that Rx.NET has beta versions of v2.3 out on NuGet, but I can't seem to find any release notes for them. Do you happen to know where they might be? – Kevin Craft Feb 04 '16 at 02:33
  • 3
    I dont think that there will be another 2.x version of Rx released. I believe that Bart and the team are working on v3.0 at the moment. Hopefully we will see something from them in the next few months/quarters. – Lee Campbell Feb 04 '16 at 02:40
  • 1
    @KevinCraft Added some detail, and yes, it will respect async/await inside `onNext`, but not block a thread. – Dorus Feb 04 '16 at 08:29
  • Thanks @Dorus. It sucks that I can't use a real IObserver for this. I really hope the Rx team adds async support soon! – Kevin Craft Feb 04 '16 at 16:04
  • 1
    I think they are avoiding it for the moment. IMO it is a bit like giving sharp knives to children. This stuff is complicated enough without mixing asynchrony paradigms mid query (Rx Schedulers vs TaskPool) – Lee Campbell Feb 05 '16 at 06:22
  • Thanks for the answer @Dorus - it was different to how i imagined it. – Nathan White Oct 12 '16 at 04:55