2

In light of some of the comments, I should make it clear that this question is about why the TestScheduler is throwing a null-reference exception, not how to get the test to pass. An earlier example assumed that interaction with the TPL was the cause of the problem but I've now discovered that this is not required to trigger the behaviour so I've replaced the code with a simpler test case

I'm having some problems trying to combine the rx-testing 'virtual time' TestScheduler with a background thread. The simplest way I've found to demonstrate the problem is shown at the bottom of the post.

The code is simply running a background thread that subscribes to an observable sequence with a timeout.

The timeout is driven from a TestScheduler and as I advance this on the main thread, a null-reference exception is being generated:

Assert.Fail failed. Virtual time 00:00:00.1394720, exception System.NullReferenceException: Object reference not set to an instance of an object. at System.Reactive.Concurrency.VirtualTimeScheduler2.GetNext() at System.Reactive.Concurrency.VirtualTimeSchedulerBase2.AdvanceTo(TAbsolute time) at System.Reactive.Concurrency.VirtualTimeSchedulerBase`2.AdvanceBy(TRelative time) at UnitTestProject1.UnitTest1.d__6.MoveNext() in .......\UnitTest1.cs:line

The failure appears to be insensitive to the exact type of IObservable used but does seem to be dependent on the presence of the Timeout selector. Interestingly though, the test typically fails at a virtual time well before the timeout is due to fire.

Running the same code as a console application also appears to work although the problem appears to be quite easily perturbed (possibly a race-condition) so this may be a red-herring.

Further research and comments imply strongly that the behaviour is due to a race condition that occurs when the scheduler is advanced as the background thread schedules its timeout action

Thanks in advance for any light you can shed on this...

using Microsoft.Reactive.Testing;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Reactive.Linq;
using System.Threading;

namespace UnitTestProject1
{
[TestClass]
public class UnitTest1
{
    [TestMethod]
    public void TestBackgroundThreadWithTestScheduler()
    {
        var scheduler = new TestScheduler();
        var seq = Observable.Never<string>();

        bool subscribed = false;
        ThreadPool.QueueUserWorkItem(_ =>
        {
            Thread.Sleep(100); //wait a bit to give main thread chance to start advancing scheduler
            seq.Timeout(TimeSpan.FromSeconds(10), scheduler)
               .Subscribe(s => {/*never called*/});
            subscribed = true; //signal we're subscribed
        });

        //----  Uncommenting this line to avoid the race condition appears to fix the test ----
        //while (!subscribed) Thread.Yield(); 

        //Advance the scheduer in small increments to maximise our chances of hitting the race
        var watch = scheduler.StartStopwatch();
        try
        {
            while (watch.Elapsed < TimeSpan.FromSeconds(20)) scheduler.AdvanceBy(10);
        }
        //NullReference is thrown unexpectedly
        catch (NullReferenceException ex)
        {
            Assert.Fail("Virtual time {0}, exception {1}", watch.Elapsed, ex);
        }
        catch (TimeoutException)
        {
            //desired result is a TimeoutException so this is a test pass
        }
    }
}
}
NeilMacMullen
  • 3,339
  • 2
  • 20
  • 22
  • Further comment - I wondered whether scheduling the timeout might not be thread-safe (there is no guarantee the background thread will have run by the time we start advancing the scheduler) but inserting a 2 second sleep after the task.factory.startnew is not enough to avoid the problem. – NeilMacMullen Oct 09 '14 at 21:19
  • 1
    I'm not sure what's going on with the NRE - but that's by the by. What exactly are you trying to test here? That's some very unusual code. You realise the `Task` in the `task` variable is never going to fail - it's job appears to be just to asynchronously start another task? Also, did you mean to be advancing the test scheduler by 10 ticks at a time? If you just want to test a Task timeout, that's easy enough - but I'll wait for clarification. – James World Oct 09 '14 at 21:24
  • This is a deliberately simple example. As a unit test in its own right you are correct that it makes little sense but it does demonstrate an unexpected exception. (As mentioned above, I was unable to demonstrate the problem in the context of a console application otherwise I would have use that form.). The line that reads 'scheduler.AdvanceBy' is moving 'virtual time' on the scheduler forward by 10 ticks at a time (i.e. it is calling any actions that are due on the TestScheduler) – NeilMacMullen Oct 09 '14 at 21:42
  • 1
    You can achieve the same goal just by calling `Start()` - any scheduled action will execute at the virtual time it has been scheduled. e.g. If you `AdvanceBy` 10 seconds then the item scheduled to execute at virtual time T=1 second will still execute at virtual time T=1 second. You don't need to bother with the small increments. I wonder if the NRE is part of an edge case that escaped testing because there's never really a need to do that. – James World Oct 09 '14 at 21:53
  • Your Debugtest method never `awaits` anything and never returns a `Task` and probably doesn't compile as written. Can you add the missing `await` just so we can clarify what is going on? – Brandon Oct 09 '14 at 22:16
  • Please see edited main post. James - the reason AdvanceTo is called in small increments is to trigger the NRE. (I stumbled across this because in the real-world code , the testscheduler is being used to 'pump' a simulation engine that has some interactions with external components.) Brnadon - you're right that the async'ness of the test is redundant and is left over from an earlier more complex version as I was trying to cut this down to the simplest repro case. Now removed entirely. – NeilMacMullen Oct 10 '14 at 08:42

1 Answers1

1

This line in the stacktrace System.Reactive.Concurrency.VirtualTimeScheduler2.GetNext() is a clue that the error is indeed that you are accessing the scheduler from 2 threads (the TestScheduler is not thread-safe there is currently a bug in TestScheduler when accessing it from multiple threads).

Your first comment is likely correct: The background task is adding to the scheduler just as your other task is advancing the scheduler.

Try adding a lock around the statements that access the scheduler and see if that solves your problem.

The real fix, of course, is to have the background Task signal once it has set itself up, and have the test task wait for the signal before continuing. Because even if the TestScheduler did not have its bug, your test has a race condition that the test thread can complete before the background thread ever subscribes to the observable.

Brandon
  • 38,310
  • 8
  • 82
  • 87
  • 1
    The `TestScheduler` *should be* threadsafe; the NRE [is a bug](https://github.com/ReactiveX/Rx.NET/issues/8). Also, the `SynchronizationContext` doesn't have anything to do with it; MSTest does not supply a `SynchronizationContext`. – Stephen Cleary Oct 09 '14 at 22:44
  • Thanks for both comments. After reading the bug description, I've now posted a much simpler repro case which avoids the TPL completely and simply uses a background thread. Yesterday, I thought I had some results which showed the problem even if the timeout was scheduled _before_ the scheduler was advanced but I don't seem to be able to reproduce that now. Locking the scheduler does appear to workaround the problem but is ugly; signalling task set-up is impractical in the real code (the point of using RX is to avoid knowledge of numbers of subscribers). – NeilMacMullen Oct 10 '14 at 08:52
  • I will leave this another day to see if more useful discussion is triggered. The answer I was looking for is really Stephen's "it's a bug" but will mark Brandon's post if a more authoritative version doesn't come along. – NeilMacMullen Oct 10 '14 at 09:01
  • @Brandon the bug link is dead (for me). It is interesting that there is a concern that the TestScheduler should be thread safe. This IMO is in direct conflict with the problem it is trying to solve; providing deterministic testing by making concurrent code just asynchronous single threaded code. If the bug is in VirtualScheduler then that may hold some more weight. – Lee Campbell Oct 15 '14 at 21:38
  • 1
    @LeeCampbell looks like the github repo was wiped. I changed the link to the codeplex issue. I personally agree with you (as indicated by my original answer) -- I would not expect the `TestScheduler` to be thread-safe. – Brandon Oct 16 '14 at 13:34