2

I have code like this in a method:

ISubject<Message> messages = new ReplaySubject<Message>(messageTimeout);

public void HandleNext(string clientId, Action<object> callback)
{
    messages.Where(message => !message.IsHandledBy(clientId))
            .Take(1)
            .Subscribe(message =>
                       {
                           callback(message.Message);
                           message.MarkAsHandledBy(clientId);
                       });
}

What is the rx'y way to code it, so that no race between MarkAsHandledBy() and IsHandledBy() may happen on multiple concurrent calls to HandleNext()?

EDIT:

This is for long polling. HandleNext() is called for each web request. The request can only handle one message and then returns to the client. Next request takes the next message and so forth.

The full code (still a work in progress of course) is this:

public class Queue
{
    readonly ISubject<MessageWrapper> messages;

    public Queue() : this(TimeSpan.FromSeconds(30)) {}

    public Queue(TimeSpan messageTimeout)
    {
        messages = new ReplaySubject<MessageWrapper>(messageTimeout);
    }

    public void Send(string channel, object message)
    {
        messages.OnNext(new MessageWrapper(new List<string> {channel}, message));
    }

    public void ReceiveNext(string clientId, string channel, Action<object> callback)
    {
        messages
            .Where(message => message.Channels.Contains(channel) && !message.IsReceivedBy(clientId))
            .Take(1)
            .Subscribe(message =>
                       {
                           callback(message.Message);
                           message.MarkAsReceivedFor(clientId);
                       });
    }

    class MessageWrapper
    {
        readonly List<string> receivers;

        public MessageWrapper(List<string> channels, object message)
        {
            receivers = new List<string>();
            Channels = channels;
            Message = message;
        }

        public List<string> Channels { get; private set; }
        public object Message { get; private set; }

        public void MarkAsReceivedFor(string clientId)
        {
            receivers.Add(clientId);
        }

        public bool IsReceivedBy(string clientId)
        {
            return receivers.Contains(clientId);
        }
    }
}

EDIT 2:

Right now my code looks like this:

public void ReceiveNext(string clientId, string channel, Action<object> callback)
{
    var subscription = Disposable.Empty;
    subscription = messages
        .Where(message => message.Channels.Contains(channel))
        .Subscribe(message =>
                   {
                       if (message.TryDispatchTo(clientId, callback))
                           subscription.Dispose();
                   });
}

class MessageWrapper
{
    readonly object message;
    readonly List<string> receivers;

    public MessageWrapper(List<string> channels, object message)
    {
        this.message = message;
        receivers = new List<string>();
        Channels = channels;
    }

    public List<string> Channels { get; private set; }

    public bool TryDispatchTo(string clientId, Action<object> handler)
    {
        lock (receivers)
        {
            if (IsReceivedBy(clientId)) return false;
            handler(message);
            MarkAsReceivedFor(clientId);
            return true;
        }
    }

    void MarkAsReceivedFor(string clientId)
    {
        receivers.Add(clientId);
    }

    bool IsReceivedBy(string clientId)
    {
        return receivers.Contains(clientId);
    }
}
Wilka
  • 28,701
  • 14
  • 75
  • 97
asgerhallas
  • 16,890
  • 6
  • 50
  • 68
  • Why do you need `HandleNext` at all? Can't you handle the whole queue in one function? `HandleNext` seems for me not very rx'y (because message is mutable this way while it shouldn't). Functional-style programming eliminates races by not using mutable things. – Vlad Sep 10 '12 at 11:19
  • Hmm... you're right, I am actually polling for messages. I must admit you've got me thinking here. I thought rx was the perfect fit for this, now I'm not so sure anymore. – asgerhallas Sep 10 '12 at 12:06
  • You code seems to be a bit contrived. Is this the actual code you're using or did you "dumb it down" for posting here? Can you explain how the calls to `HandleNext` are made? Also, just as a side note - Rx **is** probably a great fit for what you're doing, but the way you're doing it is killing it. – Enigmativity Sep 10 '12 at 12:21
  • Ok, I only dumbed it down a notch :) It actually does work very well. I have only this one race condition test failing. I updated the question now as per your comment. – asgerhallas Sep 10 '12 at 12:41
  • Well, I would propose the following: the handling code just works with the whole queue. As long as the queue is empty, the code just waits for the next message in the queue. So no need to mark the already processed messages. The semantics will be more like a pipeline. You won't need to explicitly call `HandleNext`: as soon as the message arrives into the queue, the worker will get pushed by it. – Vlad Sep 10 '12 at 12:43
  • I'm not sure I understand. How would that work, if the worker is a web request that can only handle one message before it is gone? The only thing connecting two subsequent requests is the clientId. Am I misunderstanding you, if I understand your suggestion to require that the subscriber was "always available" to handle new messages? – asgerhallas Sep 10 '12 at 12:58
  • @asgerhallas: I was thinking about something like `messages.Subscribe(msg => GetCallbackForMsg(msg).Process(msg)` or `messages.Subscribe(msg => new SomeWorker().Process(msg)` – Vlad Sep 10 '12 at 13:22
  • But the same message can be received by multiple subscribers. The `ReceiveNext` method is called each time a new "worker" is ready to handle messages. Each worker subscribes itself and can handle exactly one message. The identity of the worker is the clientId, that must ensure that each message is handled only once per clientId, but not neccessarily once per message. – asgerhallas Sep 10 '12 at 14:07
  • @asgerhallas: sorry, is that the expected design, or the status after the changes I proposed? Do you _need_ to be able to process a message by several workers, or you imply that that is a side-effect of my proposal? – Vlad Sep 10 '12 at 15:12
  • @asgerhallas: if you want to schedule different messages to be processed on different threads, you should perhaps use `ObserveOn` with some appropriate `SynchronizationContext`? (e.g., `Scheduler.ThreadPool`) – Vlad Sep 10 '12 at 15:13
  • @asgerhallas - You need to do the "@" notation to provide us with a notification that you've responded to our comments. They do not automatically get sent. – Enigmativity Sep 11 '12 at 02:54
  • @Enigmativity Oh bugger. Sorry about that, I thought you'd automatically subscribe to threads you were joined. – asgerhallas Sep 11 '12 at 06:58
  • @asgerhallas - as per my comment below - you really don't want to rely on calling `ReceiveNext` for each and every value - you're going to miss values. You need to subscribe once and have the query only return the values you need. – Enigmativity Sep 11 '12 at 07:53
  • @Enigmativity "I thought you'd automatically subscribe to threads you were joined" - here I was talking about comment-threads on SO :) – asgerhallas Sep 11 '12 at 07:56
  • @asgerhallas - Yes, I was referring to your code changes with the unsubscribe in the `ReceiveNext` method - not about the comments here. – Enigmativity Sep 11 '12 at 08:03
  • @Enigmativity Ah ok :) How am I going to miss values when I use the ReplaySubject? – asgerhallas Sep 11 '12 at 08:04
  • @asgerhallas - because if I send multiple values then only one gets through. If your callback resubscribes then you are likely to have missed those other values. I assume the send occurs on one thread. You could send thousands of messages before the resubscribe occurs. – Enigmativity Sep 11 '12 at 08:30
  • @Enigmativity oh yes, that is a real concern. I need to handle that one. The Take(1) is out of the question, and I need to be able to gather multiple messages into one web response so none will be lost. I guess that is a Throttle or the like. – asgerhallas Sep 11 '12 at 08:53
  • @Enigmativity No, not throttle, I can see. – asgerhallas Sep 11 '12 at 09:05

2 Answers2

2

It seems to me that you're making an Rx nightmare for yourself. Rx should provide a very easy way to wire up subscribers to your messages.

I like the fact that you've got a self contained class holding your ReplaySubject - that stops somewhere else in your code being malicious and calling OnCompleted prematurely.

However, the ReceiveNext method doesn't provide any way for you to remove subscribers. It is a memory leak at least. You tracking of client ids in the MessageWrapper is also a potential memory leak.

I'd suggest you try to work with this kind of function rather thanReceiveNext:

public IDisposable RegisterChannel(string channel, Action<object> callback)
{
    return messages
        .Where(message => message.Channels.Contains(channel))
        .Subscribe(message => callback(message.Message));
}

It's very Rx-ish. It's a nice pure query and you can unsubscribe easily.

Since the Action<object> callback is no doubt directly related to the clientId I'd think about putting the logic to prevent duplicate message processing in there.

Right now you code is very procedural and not suited to Rx. It seems like you haven't quite got your head around how to best work with Rx. It's a good start, but you need to think more functionally (as in functional programming).


If you must use your code as-is, I'd suggest some changes.

In Queue do this:

public IDisposable ReceiveNext(
    string clientId, string channel, Action<object> callback)
{
    return
        messages
            .Where(message => message.Channels.Contains(channel))
            .Take(1)
            .Subscribe(message =>
                message.TryReceive(clientId, callback));
}

And in MessageWrapper get rid of MarkAsReceivedFor & IsReceivedBy and do this instead:

    public bool TryReceive(string clientId, Action<object> callback)
    {
        lock (receivers)
        {
            if (!receivers.Contains(clientId))
            {
                callback(this.Message);
                receivers.Add(clientId);
                return true;
            }
            else
                return false;
        }
    }

I really don't see why you have the .Take(1) though, but these changes may reduce the race condition depending on its cause.

Enigmativity
  • 113,464
  • 11
  • 89
  • 172
  • Thank you very much for your thorough answer! As for your last suggestion, that is exactly what I have done so far, but as you say - it is not rx'y... and it uses locks. Now I have some doubts about the memory leak things: I was under the impression, that doing a .Take(1) would automatically unsubscribe after exactly one element (that why it's there). That's actually from your own answer here: http://stackoverflow.com/a/7707768/64105, maybe I misunderstood? And as for the list of clientIds, I believe that they will be purged periodically, as the ReplaySubject has a time window on it? – asgerhallas Sep 11 '12 at 06:44
  • Now about the RegisterChannel method, I'm not sure I get it. If a web request calls RegisterChannel and then receives a message, the request will respond and then close. Now if another message is coming in, there is no active request to handle the callback and the message is lost. How is RegisterChannel supposed to be used? Won't it just move the responsibility to unsubscribe into the callback-function instead? – asgerhallas Sep 11 '12 at 07:34
  • @asgerhallas - The use of `Take(1)` does indeed unsubscribe after one value, but you are now expecting that calls to `ReceiveNext` will occur immediately after each value is received. Your code doesn't do this. Even if it did though you will likely miss messages because of the multithreading nature of the code. You really want to create an Rx query that you subscribe to once and get all of your values from that. You don't ever want to keep re-subscribing like you are doing. – Enigmativity Sep 11 '12 at 07:42
  • But who's gonna get the values between requests? I don't get how the callbacks that are invoked while no request is there to handle them, is not going to be lost? – asgerhallas Sep 11 '12 at 07:47
  • @asgerhallas - I think that's the issue that you're struggling with. The query should be written so that each new value is handled by the one request - i.e. **no** `Take(1)`. Instead you should get each and every value that you need from the one subscription. – Enigmativity Sep 11 '12 at 07:57
  • Ok, to make that work, I would need to let new requests tell the single subscription that it's ready to receive a new value. How would I do that? – asgerhallas Sep 11 '12 at 08:01
  • @asgerhallas - My answer has that without the `.Take(1)` call. Each time you call `Send` your active subscriptions would receive the new value. – Enigmativity Sep 11 '12 at 08:04
  • Ok, when you say your "active subscriptions" - who are the subscribers then? – asgerhallas Sep 11 '12 at 08:13
  • @asgerhallas - If you don't unsubscribe and you don't use a `.Take(1)` then your subscribers are whoever subscribed. :-) – Enigmativity Sep 11 '12 at 08:31
  • I really appreciate your patience here! But I think we may be misunderstanding each other :) What I'm saying, and what does not seems to be addressed, is that there are no persistent subscribers. Each time a web request comes in, it will subscribe for some particular message - but _only_ for that request. As soon as the request reponds, there are no subscribers anymore. There is no initial serverside setup of client subscriptions - those come with each request (for scalability). The next time a web request comes in, it waits for new messages on the specified channel, before responding. – asgerhallas Sep 11 '12 at 08:42
  • But now this has deviated quite a lot from the original question :) Which in essence was if there is an immutable, non-shared state way to avoid that the same client receives the same message twice - even though the client is only recognized by an arbitrary id. To avoid locking on shared state. But maybe I should just ensure idempotency on the client. – asgerhallas Sep 11 '12 at 08:50
  • @asgerhallas - Now I see what you're trying to do. You're using the wrong pattern for this. You need to create a function that takes in your parameters and returns and observable - `Func>`. You need to create an instance of your web service in there and use an `AsyncSubject` to return the result of the `IObservable` so that you can have multiple "clients" use the result. Then you're not dealing with issues of state at all and you have once off calls to the web service. – Enigmativity Sep 11 '12 at 11:03
  • Thanks for staying with me! :) I must admit you lost me there. What do you mean by create the web service inside the function? When will this function be invoked? And by whom? And will there then be an AsyncSubject per message? – asgerhallas Sep 11 '12 at 12:42
  • Ok, this took me a while to grasp, but I got it now. What I really needed, which I couldn't have described at the time, was a GroupJoin. I have now moved away from having a ReceiveNext method and constantly subscribing/unsubscribing. You have been a great help to get me started. Thanks :) – asgerhallas Oct 21 '12 at 06:30
1

I'm not sure employing Rx like this is a good practice. Rx defines the concept of streams which requires there be no concurrent notifications.

That said, to answer your question, to avoid a Race condition, put a lock inside the IsReceivedBy and MarkAsReceivedFor methods.

As for a better approach, you could abandon the whole handling business, use a ConcurrentQueue and TryDequeue a message on receiving a request (you're only doing Take(1) - which fits a queue model). Rx can help you to give each message a TTL and remove it from the queue, but you could also do that on an incoming request.

Asti
  • 12,447
  • 29
  • 38
  • Thanks for your answer. I'd rather not use a lock - I'm under the impression, that that would not be the rx way, and I'm trying to learn it :) ... but if I used locks, it would not help to put them in those methods respectively. I would need to lock either the whole ReceiveNext method or as @Enigmativity does in the answer above, or else the race would still occur. To understand your suggestion about the queue: If I dequeue it, then only one client can handle it, and I want multiple clients to be able to handle it (as in pub/sub). Or am I misunderstanding you? – asgerhallas Sep 11 '12 at 06:55
  • @asgerhallas - Rx uses a lot of locks under the hood. Don't be afraid of locks, just be wary of them. Whenever you are using a lock you are trying to solve a concurrency issue and these are almost impossible to get right even when you think you have. Always second guess yourself with regard to locking. – Enigmativity Sep 11 '12 at 07:37
  • @asgerhallas - Oh, and putting a lock in the `IsReceivedBy` and `MarkAsReceivedFor` method will **not** solve the race condition. Both must be in a single lock hence my `TryReceive` method. – Enigmativity Sep 11 '12 at 07:38
  • @Enigmativity yes, wasn't that what I just said in my comment above? :) ... now it's not that I don't know how to do locking appropriately, I just thought that rx would have operators that matched my scenario, so that I did not need to be concerned about lokcing if I just molded it the right way. I like locking best when it's under the hood. – asgerhallas Sep 11 '12 at 07:44
  • @asgerhallas - You're not using Rx in the right way so you can't expect the locking you're using to work properly. Keep locking atomic in the very least. Preferably get rid of the locking. My answer shows a way without locking. – Enigmativity Sep 11 '12 at 07:55
  • @Asti 1. Thread A calls IsReceivedBy and get false and releases the lock, 2. Thread B calls IsReceivedBy and get false and releases the lock, 3. Thread A's onNext function is called and now MarkAsReceived is called, but it's too late, 'cause Thread B is already let through. – asgerhallas Sep 11 '12 at 08:07
  • Ah, sorry - I was thinking of a case in which the where clause was combined into the callback - Enigmativity's idea of a TryReceive is a good one. I was confused by "The request can only handle one message and then returns to the client. Next request takes the next message and so forth." - hence the suggestion for a queue. – Asti Sep 11 '12 at 08:27
  • @Asti - As per asgerhallas comment. :-) – Enigmativity Sep 11 '12 at 08:29