3

I wanted to check that an IObservable I had created was respecting the courtesy of "Once I've completed, I'll unsubscribe you". At first blush it looked like something was wrong with my code. But eliminating my code, and just using the Observable and Observer provided by TestScheduler, it looks like the 'unsubscription' never happens:

using Microsoft.Reactive.Testing;
using System.Reactive;
...
var ts = new TestScheduler();
var ob = ts.CreateObserver<int>();
var xs = ts.CreateColdObservable<int>(
    new Recorded<Notification<int>>(1, Notification.CreateOnCompleted<int>())
    );

xs.Subscribe(ob);
ts.AdvanceTo(2);
Assert.Equal(1, xs.Subscriptions.Single().Unsubscribe); //<-- Xunit no like

I originally suspected the observer, but I tried that on a variant of the code found here, and it works, so now I'm thinking that the implementation of Subscribe on the ColdObservable isn't behaving properly.

Community
  • 1
  • 1
Benjol
  • 63,995
  • 54
  • 186
  • 268

1 Answers1

2

No such courtesy exists. The RX design guideines in section 4.3 suggest you can:

Assume resources are cleaned up after an OnError or OnCompleted message.

And in section 4.4 say you can:

Assume a best effort to stop all outstanding work on Unsubscribe

These guidelines ("courtesies") talk about an operator releasing it's own resources plus those of any it has acquired as soon as possible.

In your code, you aren't testing for either of these scenarios. The purpose of the Unsubscribe property on an ITestableObservable is to report when a subscription taken out by an observer was explicitly disposed, not when internal cleanup happened - but you are not storing this handle to be able to dispose it:

xs.Subscribe(ob); /* return of handle ignored here */

So you are trying to assert that you disposed the subscription you threw away, not that the observable you subscribed to cleaned up any subscription and resources it may have taken out.

If you want to see the effect of the timely resource clean up of 4.3/4.4, write an extension method like this:

public static IObservable<T> SpyResourceCleanUp<T>(
    this IObservable<T> source, IScheduler scheduler)
{
    return Observable.Create<T>(obs =>
    {
        var subscription = source.Subscribe(obs);
        return new CompositeDisposable(
            subscription,
            Disposable.Create(() => Console.WriteLine(
                "Clean up performed at " + scheduler.Now.Ticks)));
    });
}

And replace your line:

xs.Subscribe(ob);

with

xs.SpyResourceCleanUp(ts).Subscribe(ob);

(Editing in some of the comments)

On your test I see immediate resource clean-up, as I would expect. And with this change your test will now pass because SpyResourceCleanUp is unsubscribing from it's parent (xs) as soon as it OnCompletes() itself in adherence to 4.3 of the guidelines.

What might not be obvious here is that Observable.Create handles calling the Dispose() method of the returned IDisposable as soon as either the subscription is disposed or OnComplete() or OnError() has been called on the observer. This is how Create helps you implement section 4.3, and why the test passes with the altered code.

Under the covers, subscriptions to the AnonymousObservable<T> : ObservableBase<T> returned by Create are wrapped by an AutoDetachObserver as you can see here.

i.e. The Disposable you return from Observable.Create isn't the one the caller gets - they get a wrapped version that will call your Dispose() either on stream termination or cancellation.

James World
  • 29,019
  • 9
  • 86
  • 120
  • Yeah, but precisely. If I do replace the code as you suggest, my test now passes, and I can't work out *why*. – Benjol Dec 03 '13 at 14:03
  • 1
    What might not be obvious here is that `Observable.Create` handles calling the `Dispose()` method of the returned `IDisposable` as soon as EITHER the subscription is disposed OR `OnComplete()` or `OnError()` has been called on the observer. This is how `Create` helps you implement section 4.3. – James World Dec 03 '13 at 14:16
  • So it's Observable.Create that's doing the 'courtesy' thing, and I conflated my vague memories of that with it being a generalised thing. So it is actually the Observable.Create in you Spy which is invoking the 'Dispose' without being asked to by the Observer. As far as I can see through the callstack, it actually does this by inserting an intermediate Observer (`System.Reactive.AutoDetachObserver`) – Benjol Dec 03 '13 at 14:19
  • Yes, that setup takes place in [`ObservableBase`](http://rx.codeplex.com/SourceControl/latest#Rx.NET/Source/System.Reactive.Core/Reactive/ObservableBase.cs) which [`AnonymousObservable`](http://rx.codeplex.com/SourceControl/latest#Rx.NET/Source/System.Reactive.Core/Reactive/AnonymousObservable.cs) subclasses. This is used by the [`Create`](http://rx.codeplex.com/SourceControl/latest#Rx.NET/Source/System.Reactive.Linq/Reactive/Linq/QueryLanguage.Creation.cs) implementation but note this implementation could be replaced by PlatformServices. I don't *think* it is overridden anywhere currently. – James World Dec 03 '13 at 14:51
  • Edited the pertinent bits of this discussion into my answer. – James World Dec 03 '13 at 15:19