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.