0

My goal is to have an update handler be called no more than once per 5 seconds for "Type2" objects. The observable will be generating more than one value per 5 seconds but I want to ignore all of them that occur within 5 seconds of the last handled update.

I asked this question here: Throttle only if specific condition met

and got good feedback. It led me to use Observable.Window to try to achieve my goal. I thought I had it working but it turns out that it can produce incorrect output if the first update comes right before a window closes (so the update is handled) and then once the next window opens, another update arrives and is also handled, when I don't want it to since it came within 5 seconds of the last handled update.

Here is some code to demonstrate the issue, slightly modified from the code in the link:

var source = new Subject<Thing>();    
var feed = source.Publish().RefCount();

var ofType1 = feed.Where(t => t.ActivationType == "Type1");
var ofType2 = feed
    .Where(t => t.ActivationType == "Type2")
    .Window(() =>
            Observable.Timer(TimeSpan.FromSeconds(5))
            .Do(t => Console.WriteLine("\nTICK: " + DateTime.Now.ToString("hh:mm:ss:fff"))))
    .Select(x => x.Take(1))
    .Merge()
    .Do(t => Console.WriteLine("A new window opened " + DateTime.Now.ToString("hh:mm:ss:fff")));


var query = ofType1.Merge(ofType2);        
query.Subscribe(t => Console.WriteLine("UPDATE: " + t.ID + " " + DateTime.Now.ToString("hh:mm:ss:fff")));    

int msDelay = 3000;
Task task = Task.Factory
    .StartNew(() => { Thread.Sleep(msDelay); })
    .ContinueWith((Task starter) =>
        {
            while (running)
            {
                var thing = new Thing();  //Note that all Things are by default Type2
                source.OnNext(thing);
                Thread.Sleep(100);
            }
        }, TaskContinuationOptions.LongRunning);

Console.ReadLine();

So, the subscription is made and once the subscription is made, the Observable.Timer used in the Window begins. The while loop that is used to produce values does not begin until after a 3000 ms delay.

The output looks like:

A new window opened 03:48:03:725
UPDATE: 1ac54fb3-f73d-4840-b4d8-95d4250ce65d 03:48:03:752

TICK: 03:48:05:714
A new window opened 03:48:05:754
UPDATE: 12d36e53-010f-4ccd-b9f8-2951b085f88c 03:48:05:754

TICK: 03:48:10:730
A new window opened 03:48:10:755
UPDATE: 25d84e72-94f9-4f50-83f4-14c1004c10fa 03:48:10:755

TICK: 03:48:15:738
A new window opened 03:48:15:755
UPDATE: 5f32b7d5-196f-445c-bf25-5c362b2fd6f0 03:48:15:755

TICK: 03:48:20:747
A new window opened 03:48:20:756
UPDATE: e3a3a30d-8031-41b5-b115-499dbe91aaf7 03:48:20:756

TICK: 03:48:25:755
A new window opened 03:48:25:756
UPDATE: 239fb25b-5135-463b-bf7e-5728ffa07f5c 03:48:25:756

As you can see, the first Type2 update comes in while a window is open, so it gets handled. Then, 2 secs later, the Window's timer ticks and a new window is opened. It immediately handles the next Type2 update, which I don't want it to do. After that it looks to work normally (one update every 5 secs as defined in the Window declaration).

Is there a way or another method I can use to make sure that only one update per 5 seconds (or whatever timeframe I choose) is ever handeld?

Community
  • 1
  • 1
Flack
  • 5,727
  • 14
  • 69
  • 104
  • If you only want 1 "type 2" event to be processed per minute, you need to change the window length selector - instead of 5 seconds, make it 60 seconds. :) – JerKimball Mar 07 '13 at 21:48
  • Oops, just a typo on my part. I will edit my original question but I was doing my testing with a window length selector of 5 secs. Though even if I set it to 60 secs the problem still occurs. – Flack Mar 07 '13 at 21:51
  • oh crap, I understand what you're after now...lemme think it over a bit...sorry about the misunderstanding – JerKimball Mar 08 '13 at 01:10
  • You don't need to Publish when the source is a Subject as there can not be any subscription side effects i.e. a Subject is already Hot. – Lee Campbell Mar 22 '13 at 18:27
  • FYI: Your whole production of values using that taks factory and Thread.Sleeps (yuck!) can be replaced with Observable.Timer(TimeSpan.FromSeconds(3),TimeSpan.FromMilliseconds(300), Scheduler.TaskPool).Select(_=>new Thing()).Subscribe(source); – Lee Campbell Mar 22 '13 at 18:29
  • This then sugests that you dont need the subject in the first place, which now brings us back to having a Publish/Refcount. --> var source = Observable.Timer(TimeSpan.FromSeconds(3), TimeSpan.FromMilliseconds(300), Scheduler.TaskPool).Select(_=>new Thing()).Publish().RefCount(); – Lee Campbell Mar 22 '13 at 18:31

1 Answers1

1

I think I have a solution, but first can I make some suggestions. I think there is a lot of noise in the question that makes it hard to find what the real question is.

Effectively you are asking "How can I get a value followed by silence for at least 5 seconds". The Type1 code is a distraction. The generation of the sequence is a distraction too.

So lets clean up the sample code and see if we can see the wood for the trees:

First, I dont think it is entirely relevant what the type is that is coming through. In your example we never push a Type1, so lets just use integers instead. It might make it easier.

Next we can clean up the creation by just using an Observable.Timer instead of the big loop+task+thread.sleep stuff.

Now we have a simple starting place:

var source = Observable.Timer(TimeSpan.FromSeconds(3),TimeSpan.FromMilliseconds(300), Scheduler.TaskPool);   
var feed = source.Publish().RefCount();

So our first problem is a misunderstanding of the Window overload you are using. I think your expectation is that it will open a window when the first value is pushed. That is not the case. The Timer (i.e. Observable.Timer(TimeSpan.FromSeconds(5))) is subscribed to each time a window opens, which is initially when the subscription happens, and then again as the window itself closes. Thus, the timer starts immediately and you only will get 2seconds of values for the first window.

Next, I would draw out my problem space. My favourite way to do this is with marble diagrams. They dont translate so well in ASCII but lets try any way.

Given this input sequence:

//Seconds             1111111111222222222233333333334444444444555555555566666666667777777777888888888899999999990000000000
//Tenths    01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890
//
//source  : ------------------------------0--1--2--3--4--5--6--7--8--9--0--1--2--3--4--5--6--7--8--9--0--1--2--3--4--5--6--7--8--9--0

This is supposed to represent the value '0' being produced at 3.0 seconds. Followed by a '1' at 3.3 seconds etc..

Now with a bit of clarity of the problem space we can draw up where we think a window should open, where it should close and where the next window should open.

Lets look at what we want instead.

Here we add a Window1 (W1) that opens when the first value is pushed ('1' @3.0s). It closes 5 seconds later. During this window we want silence after that first value of 1.

Window 2 (W2) should then open once the next value is produced, not just immediately after the last window closes (I think?!). Here we see that this is Opened when the value '17' is pushed at time 8.4seconds.

//Seconds             1111111111222222222233333333334444444444555555555566666666667777777777888888888899999999990000000000
//Tenths    01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890

//source  : ------------------------------0--1--2--3--4--5--6--7--8--9--1--1--1--1--1--1--1--1--1--1--2--2--2--2--2--2--2--2--2--2--3--3--3--3--3--3--3--3--3--3--4--4--4--4-
                                                                        0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5  6  7  8  9  0  1  2  3
//
//W1      :                               0-------------------------------------------------|
//W2      :                                                                                  (17)----------------------------------------------|
//W3      :                                                                                                                                     (34)------------------------>

//expected: ------------------------------0--------------------------------------------------1--------------------------------------------------3---------------------------
                                                                                             7                                                  4

Now that we now what values we are looking for, we can construct a query.

I came up with this. I make the assumption that the feed is in fact a Hot sequence. Using this assumption, I construct a repeating stucture that takes 1 value from the feed and the concatenates 5 seconds of silence to the sequence. I then add the Repeat operator, which will only resubscribe to the feed once the 5 seconds of silence has lapsed.

public static IObservable<T> Silencer<T>(this IObservable<T> source, TimeSpan minSilencePeriod)
{
    return source.Take(1)
                 .Concat(Observable.Empty<T>().Delay(minSilencePeriod))
                 .Repeat();
}

This does produce values 0, 17, 51 etc as expected.

Now applying this to the code from the original question (cleaning up some stuff)

void Main()
{
    var source = Observable.Timer(TimeSpan.FromSeconds(3),TimeSpan.FromMilliseconds(300), Scheduler.TaskPool).Select(_=>new Thing());   
    var feed = source.Publish().RefCount();

    var ofType1 = feed.Where(t => t.ActivationType == "Type1");
    var ofType2 = feed
            .Where(t => t.ActivationType == "Type2")
            .Silencer(TimeSpan.FromSeconds(5));


    var query = ofType1.Merge(ofType2);        
    var subscription = query.Subscribe(t => Console.WriteLine("UPDATE: " + t.ID + " " + DateTime.Now.ToString("hh:mm:ss:fff")));    


    Console.ReadLine();
    subscription.Dispose();
}

We see output that has values at least 5 seconds apart

UPDATE: 3f0fc6f3-8a5a-476f-9661-b7330ab77877 09:14:04:725
UPDATE: fc8f0025-7a79-4329-8164-b8b421ad5865 09:14:09:817
UPDATE: ad739a71-885e-4d5b-a352-2302df0a4d87 09:14:14:925
Lee Campbell
  • 10,631
  • 1
  • 34
  • 29