-1

I'm trying to learn RX(.net) and I'm losing my mind a bit. I have an observable of which I want to handle exceptions by using Catch(). I want to be able to access the item T that is moving through the observable chain within that Catch() and I thought this would be possible with a higher order observable that is Concat()ed afterwards, like so:

IObservable<RunData> obs = ...;
var safeObs = obs.Select(rd =>
  .Select(rd => {
    // simple toy example, could throw exception here in practice
    // throw new Exception();
    return (result: true, runData: rd);
  })
  .Catch((Exception e) => // try to catch any exception occurring within the stream, return a new tuple with result: false if that happens
  {
    return (Observable.Return((result: false, runData: rd))); // possible to access rd here
  })
).Concat();

So far, so good.

But while testing this pattern I noticed that it breaks the assumption that I'm able to see all RunData instances when I subscribe to that safeObs. I've written the following test to showcase this:

[Test]
[Explicit]
public async Task TestHigherOrderExceptionHandling()
{
    var counter = new Counter();

    var useHigherOrderExceptionHandling = true; // test succeeds when false, fails when true

    var obs = Observable.Create<RunData>(async (o) =>
    {
        await Task.Delay(100); // just here to justify the async nature

        o.OnNext(new RunData(counter)); // produce a new RunData object, must be disposed later!

        o.OnCompleted();

        return Disposable.Empty;
    })
        .Concat(Observable.Empty<RunData>().Delay(TimeSpan.FromSeconds(1)))
        .Repeat() // Resubscribe indefinitely after source completes
        .Publish().RefCount() // see http://northhorizon.net/2011/sharing-in-rx/
        ;

    // transforms the stream, exceptions might be thrown inside of stream, would like to catch them and handle them appropriately
    IObservable<(bool result, RunData runData)> TransformRunDataToResult(IObservable<RunData> obs)
    {
        return obs.Select(rd => {
            // simple toy example, could throw exception here in practice
            // throw new Exception();
            return (result: true, runData: rd);
        });
    }

    IObservable<(bool result, RunData runData)> safeObs;
    if (useHigherOrderExceptionHandling)
    {
        safeObs = obs.Select(rd =>
            TransformRunDataToResult(obs)
            .Catch((Exception e) => // try to catch any exception occurring within the stream, return a new tuple with result: false if that happens
            {
                return (Observable.Return((result: false, runData: rd)));
            })
        ).Concat();
    }
    else
    {
        safeObs = TransformRunDataToResult(obs);
    }

    safeObs.Subscribe(
        async (t) =>
        {
            var (result, runData) = t;
            try
            {
                await Task.Delay(100); // just here to justify the async nature

                Console.WriteLine($"Result: {result}");
            }
            finally
            {
                t.runData.Dispose(); // dispose RunData instance that was created by the observable above
            }
        });

    await Task.Delay(5000); // give observable enough time to produce a few items

    Assert.AreEqual(0, counter.Value);
}

// simple timer class, just here so we have a reference typed counter that we can pass around
public class Counter
{
    public int Value { get; set; }
}

// data that is moving through observable pipeline, must be disposed at the end
public class RunData : IDisposable
{
    private readonly Counter counter;

    public RunData(Counter counter)
    {
        this.counter = counter;

        Console.WriteLine("Created");
        counter.Value++;
    }

    public void Dispose()
    {
        Console.WriteLine("Dispose called");
        counter.Value--;
    }
}

Running this test fails. There is one more instance of RunData created than there is disposed... why? Changing useHigherOrderExceptionHandling to false makes the test succeed.

EDIT:

I simplified the code (removed async code, limited repeats to make it predictable) and tried the suggestion, but I'm getting the same bad result... the test fails:

[Test]
[Explicit]
public async Task TestHigherOrderExceptionHandling2()
{
    var counter = new Counter();
    var useHigherOrderExceptionHandling = true; // test succeeds when false, fails when true

    var obs = Observable.Create<RunData>(o =>
    {
        o.OnNext(new RunData(counter)); // produce a new RunData object, must be disposed later!
        o.OnCompleted();
        return Disposable.Empty;
    })
        .Concat(Observable.Empty<RunData>().Delay(TimeSpan.FromSeconds(1)))
        .Repeat(3) // Resubscribe two more times after source completes
        .Publish().RefCount() // see http://northhorizon.net/2011/sharing-in-rx/
        ;

    // transforms the stream, exceptions might be thrown inside of stream, I would like to catch them and handle them appropriately
    IObservable<(bool result, RunData runData)> TransformRunDataToResult(IObservable<RunData> obs)
    {
        return obs.Select(rd =>
        {
            // simple toy example, could throw exception here in practice
            // throw new Exception();
            return (result: true, runData: rd);
        });
    }

    IObservable<(bool result, RunData runData)> safeObs;
    if (useHigherOrderExceptionHandling)
    {
        safeObs = obs.Publish(_obs => _obs
            .Select(rd => TransformRunDataToResult(_obs)
                .Catch((Exception e) => Observable.Return((result: false, runData: rd)))
            ))
            .Concat();
    }
    else
    {
        safeObs = TransformRunDataToResult(obs);
    }

    safeObs.Subscribe(
        t =>
        {
            var (result, runData) = t;
            try
            {
                Console.WriteLine($"Result: {result}");
            }
            finally
            {
                t.runData.Dispose(); // dispose RunData instance that was created by the observable above
            }
        });

    await Task.Delay(4000); // give observable enough time to produce a few items

    Assert.AreEqual(0, counter.Value);
}

Output:

Created
Created
Result: True
Dispose called
Created
Result: True
Dispose called

There's still a second subscription happening at the beginning(?) and there's one more RunData object created than is disposed.

Maximilian Csuk
  • 584
  • 1
  • 6
  • 21
  • You want to avoid exception handling down the pipeline if it can be handled within a single operation in the pipeline. So, in your case, handle exceptions totally within `TransformRunDataToResult`. `Catch` in Rx is for situations where you're dealing with code that cannot reasonably control - like where some other developer has handed you a library that doesn't handle its own exceptions. You are 100% in control - so you handle the except and only let Rx see clean values in its pipeline. Make sense? – Enigmativity Nov 02 '22 at 04:17

1 Answers1

2

It's not clear here what you're trying to accomplish.

First, your code mixes Tasks with Observables, which is generally something to avoid. You generally want to pick one or the other.

Second, I found that both versions of safeObs would fail tests, as I would expect: You're consistently incrementing, then consistently decrementing, but with (effectively) inconsistent time gaps in between the increments and decrements. Run this enough times and you'll be wrong eventually.

You also have a multiple subscription bug. If you collapse all your code into one fluid chain, this bug should stand out:

// this is roughly equivalent to var obs in your code
var obs2 = Observable.Interval(TimeSpan.FromSeconds(2))
    .Select(_ => new RunData(counter))
    .Publish()
    .RefCount();

// this is equivalent to the higher order version of safeObs in your code
var safeObs2HigherOrder = obs2
    .Select(rd => obs2
        .Select(innerRd => (result: true, runData: innerRd))
        .Catch((Exception e) => Observable.Return((result: false, runData: rd)))
    ))
    .Concat();

Notice how safeObs2HigherOrder references obs2 twice, effectively subscribing twice. You can fix that as follows:

var safeObs2HigherOrder = obs.Publish(_obs => _obs
    .Select(rd => _obs
        .Select(innerRd => (result: true, runData: innerRd))
        .Catch((Exception e) => Observable.Return((result: false, runData: rd)))
    ))
    .Concat();

Lastly, the Concat at the end of the safeObs2HigherOrder should probably be a Switch or a Merge. Hard to tell when the larger problem aren't readily apparent.

I don't know if you were looking for a code review or an answer to a particular question, but your code needs quite a bit of work.

Shlomo
  • 14,102
  • 3
  • 28
  • 43
  • Thanks a lot for your help! As you can tell, I still got a lot to learn about RX. Doesn't help that the existing codebase I want to integrate it in is async throughout, which I now learned doesn't mesh well with RX. My example was badly chosen, and the exception handling I took from this answer: https://stackoverflow.com/questions/74172099/rx-net-disposing-of-resources-created-during-observable-create But you found what I actually wanted to find: the double subscription bug. I didn't realize subscribing works this way, but it's a bit clearer to me now. Gotta learn what Publish() does too. – Maximilian Csuk Oct 29 '22 at 20:34
  • I tried to implement your suggestion and simplified my code, but I'm still getting the same results... did I do it wrong? – Maximilian Csuk Oct 29 '22 at 21:16