1

I'm trying to write a notification system between a server and multiple clients using gRPC server streaming in protobuf-net.grpc (.NET Framework 4.8).

I based my service off of this example. However, if I understand the example correctly, it is only able to handle a single subscriber (as _subscriber is a member variable of the StockTickerService class).

My test service looks like this:

private readonly INotificationService _notificationService;
private readonly Channel<Notification> _channel;

public ClientNotificationService(INotificationService notificationService)
{
    _notificationService = notificationService;
    _notificationService.OnNotification += OnNotification;
    _channel = Channel.CreateUnbounded<Notification>();
}

private async void OnNotification(object sender, Notification notification)
{
    await _channel.Writer.WriteAsync(notification);
}

public IAsyncEnumerable<Notification> SubscribeAsync(CallContext context = default)
{
    return _channel.AsAsyncEnumerable(context.CancellationToken);
}

INotificationService just has an event OnNotification, which is fired when calling its Notify method.

I then realized that System.Threading.Channels implements the Producer/Consumer pattern, but I need the Publisher/Subscriber pattern. When trying it out, indeed only one of the clients gets notified, instead of all of them. It would also be nice if the server knew when a client disconnects, which seems impossible when returning _channel.AsAsyncEnumerable.

So how can I modify this in order to

  • serve multiple clients, with all of them being notified when OnNotification is called
  • and log when a client disconnects?
4b0
  • 21,981
  • 30
  • 95
  • 142
user2727133
  • 149
  • 1
  • 12

2 Answers2

1

For 1, you'd need an implementation of a publisher/subscriber API; each call to SubscribeAsync will always represent a single conversation between gRPC endpoints, so you'll need your own mechanism for broadcasting that to multiple consumers. Maybe RX is worth investigating there

For 2, context.CancellationToken should be triggered by client-disconnect

Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • Thanks for pointing me towards RX. I rewrote my NotificationService to hold a Subject, so now in ```SubscribeAsync``` I create a dedicated channel and pass ```n => channel.Writer.WriteAsync(n, context.CancellationToken)``` to my NotificationService as the OnNext action. Then the method returns ```channel.AsAsyncEnumerable(context.CancellationToken)``` (is it correct to use the same token?). I still don't know how to log when the token is cancelled, as with the return I lose control of it. And I'm unable to use the IDisposable used for unsubscribing from the NotificationService. – user2727133 Nov 11 '22 at 19:13
  • @user2727133 just don't "using" the register call; then it should get invoked even though you've exited that scope – Marc Gravell Nov 11 '22 at 20:41
  • I know, and that works, but then how do I unsubscribe? – user2727133 Nov 11 '22 at 21:58
  • 1
    @user2727133 pass the result of register to whatever needs to unsubscribe; or rewrite the thing that currently returns a sequence to *be* a sequence, and use "using" – Marc Gravell Nov 12 '22 at 13:48
  • Thanks for your help, I accepted this answer and added the solution to my question – user2727133 Nov 13 '22 at 05:06
0

Many thanks to Marc Gravell

I rewrote the NotificationService like this, using System.Reactive.Subjects (shortened) - no need for an event, use an Action instead:

public class NotificationService<T>
{
    private readonly Subject<T> _stream = new Subject<T>();

    public void Publish(T notification)
    {
        _stream.OnNext(notification);
    }

    public IDisposable Subscribe(Action<T> onNext)
        return _stream.Subscribe(onNext);
    }
}

My updated ClientNotificationService, which is exposed as a code-first gRPC service:

public class ClientNotificationService : IClientNotificationService
{
    private readonly INotificationService<Notification> _notificationService;

    public ClientNotificationService(INotificationService<Notification> notificationService)
    {
        _notificationService = notificationService;
    }

    public async IAsyncEnumerable<Notification> SubscribeAsync(CallContext context = default)
    {
        try
        {
            Channel<Notification> channel = Channel.CreateUnbounded<Notification>(
                new UnboundedChannelOptions { SingleReader = true, SingleWriter = true });

            CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(context.CancellationToken);

            using (_notificationService.Subscribe(n => channel.Writer.WriteAsync(n, cts.Token)))
            {
                await foreach (Notification notification in channel.AsAsyncEnumerable(cts.Token))
                {
                    yield return notification;
                }
            }
        }
        finally
        {
            // canceled -> log, cleanup, whatever
        }
    }
}

Note: Solution provided by OP on question section.

4b0
  • 21,981
  • 30
  • 95
  • 142