Skip to content

Commit 3507d5e

Browse files
authored
Allow opt-out from TaskScheduler.UnobservedExceptions (#1914)
1 parent 331a1a0 commit 3507d5e

File tree

18 files changed

+1653
-137
lines changed

18 files changed

+1653
-137
lines changed

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,3 +189,8 @@ nuget.exe
189189

190190
# JetBrains Rider adds these
191191
.idea/
192+
193+
# Local NCrunch settings
194+
*.v3.ncrunchproject
195+
*.v3.ncrunchsolution
196+
/Rx.NET/Source/.NCrunch_*/StoredText

Rx.NET/Source/src/Microsoft.Reactive.Testing/Microsoft.Reactive.Testing.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
<PackageTags>Rx;Reactive;Extensions;Observable;LINQ;Events</PackageTags>
99
<Description>Reactive Extensions (Rx) for .NET - Testing Library</Description>
1010
<!-- NB: A lot of CA warnings are disabled because of the .cs files included from xunit.assert.source. -->
11-
<NoWarn>$(NoWarn);IDE0054;IDE0066;CA1305;CA1307;CA1032;CA1064;CA1822;CA1812;CA1823</NoWarn>
11+
<NoWarn>$(NoWarn);IDE0054;IDE0066;CA1305;CA1307;CA1032;CA1064;CA1822;CA1812;CA1820;CA1823</NoWarn>
1212
</PropertyGroup>
1313

1414
<ItemGroup>
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT License.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System.Threading.Tasks;
6+
7+
namespace System.Reactive.Concurrency
8+
{
9+
/// <summary>
10+
/// Controls how completion or failure is handled when a <see cref="Task"/> or
11+
/// <see cref="Task{TResult}"/> is wrapped as an <see cref="IObservable{T}"/> and observed by
12+
/// an <see cref="IObserver{T}"/>.
13+
/// </summary>
14+
/// <remarks>
15+
/// <para>
16+
/// This type can be passed to overloads of the various method that adapt a TPL task as an
17+
/// <see cref="IObservable{T}"/>. It deals with two concerns that arise whenever this is done:
18+
/// the scheduler through which notifications are delivered, and the handling of exceptions
19+
/// that occur after all observers have unsubscribed.
20+
/// </para>
21+
/// <para>
22+
/// If the <see cref="Scheduler"/> property is non-null, it will be used to deliver all
23+
/// notifications to observers, whether those notifications occur immediately (because the task
24+
/// had already finished by the time it was observed) or they happen later.
25+
/// </para>
26+
/// <para>
27+
/// The <see cref="IgnoreExceptionsAfterUnsubscribe"/> property determines how to deal with tasks
28+
/// that fail after unsubscription (i.e., if an application calls <see cref="IObservable{T}.Subscribe(IObserver{T})"/>
29+
/// on an observable wrapping, then calls Dispose on the result before that task completes, and
30+
/// the task subsequently enters a faulted state). Overloads that don't take a <see cref="TaskObservationOptions"/>
31+
/// argument do not observe the <see cref="Task.Exception"/> in this case, with the result that
32+
/// the exception will then emerge from <see cref="TaskScheduler.UnobservedTaskException"/>
33+
/// (which could terminate the process, depending on how the .NET application has been
34+
/// configured). This is consistent with how unobserved <see cref="Task"/> failures are
35+
/// normally handled, but it is not consistent with how Rx handles post-unsubcription failures
36+
/// in general. For example, if the projection callback for Select is in progress at the moment
37+
/// an observer unsubscribes, and that callback then goes on to throw an exception, that
38+
/// exception is simply swallowed. (One could argue that it should instead be sent to some
39+
/// application-level unhandled exception handler, but the current behaviour has been in place
40+
/// for well over a decade, so it's not something we can change.) So there is an argument that
41+
/// post-unsubscribe failures in <see cref="IObservable{T}"/>-wrapped tasks should be
42+
/// ignored in exactly the same way: the default behaviour for post-unsubscribe failures in
43+
/// tasks is inconsistent with the handling of all other post-unsubscribe failures. This has
44+
/// also been the case for over a decade, so that inconsistency of defaults cannot be changed,
45+
/// but the <see cref="IgnoreExceptionsAfterUnsubscribe"/> property enables applications to
46+
/// ask for task-originated post-unsubscribe exceptions to be ignored in the same way as
47+
/// non-task-originated post-unsubscribe exceptions are. (Where possible, applications should
48+
/// avoid getting into situations where they throw exceptions in scenarios where nothing is
49+
/// able to observe them is. This setting is a last resort for situations in which this is
50+
/// truly unavoidable.)
51+
/// </para>
52+
/// </remarks>
53+
public sealed class TaskObservationOptions
54+
{
55+
public TaskObservationOptions(
56+
IScheduler? scheduler,
57+
bool ignoreExceptionsAfterUnsubscribe)
58+
{
59+
Scheduler = scheduler;
60+
IgnoreExceptionsAfterUnsubscribe = ignoreExceptionsAfterUnsubscribe;
61+
}
62+
63+
/// <summary>
64+
/// Gets the optional scheduler to use when delivering notifications of the tasks's
65+
/// progress.
66+
/// </summary>
67+
/// <remarks>
68+
/// If this is null, the behaviour depends on whether the task has already completed. If
69+
/// the task has finished, the relevant completion or error notifications will be delivered
70+
/// via <see cref="ImmediateScheduler.Instance"/>. If the task is still running (or not yet
71+
/// started) at the instant at which it is observed through Rx, no scheduler will be used
72+
/// if this property is null.
73+
/// </remarks>
74+
public IScheduler? Scheduler { get; }
75+
76+
/// <summary>
77+
/// Gets a flag controlling handling of exceptions that occur after cancellation
78+
/// has been initiated by unsubscribing from the observable representing the task's
79+
/// progress.
80+
/// </summary>
81+
/// <remarks>
82+
/// If this is <c>true</c>, exceptions that occur after all observers have unsubscribed
83+
/// will be handled and silently ignored. If <c>false</c>, they will go unobserved, meaning
84+
/// they will eventually emerge through <see cref="TaskScheduler.UnobservedTaskException"/>.
85+
/// </remarks>
86+
public bool IgnoreExceptionsAfterUnsubscribe { get; }
87+
88+
internal Value ToValue() => new Value(this.Scheduler, this.IgnoreExceptionsAfterUnsubscribe);
89+
90+
/// <summary>
91+
/// Value-type representation.
92+
/// </summary>
93+
/// <remarks>
94+
/// <para>
95+
/// The public API surface area for <see cref="TaskObservationOptions"/> is a class because
96+
/// using a value type would run into various issues. The type might appear in expression
97+
/// trees due to use of <see cref="System.Reactive.Linq.IQbservable{T}"/>, which limits us
98+
/// to a fairly old subset of C#. It means we can't use the <c>in</c> modifier on
99+
/// parameters, which in turn prevents us from passing options by reference, increasing the
100+
/// overhead of each method call. Also, options types such as this aren't normally value
101+
/// types, so it would be a curious design choice.
102+
/// </para>
103+
/// <para>
104+
/// The downside of using a class is that it entails an extra allocation. Since the feature
105+
/// for which this is designed (the ability to swallow unhandled exceptions thrown by tasks
106+
/// after unsubscription) is one we don't expect most applications to use, that shouldn't
107+
/// be a problem. However, to accommodate this feature, common code paths shared by various
108+
/// overloads need the information that a <see cref="TaskObservationOptions"/> holds. The
109+
/// easy approach would be to construct an instance of this type in overloads that don't
110+
/// take one as an argument. But that would be impose an additional allocation on code that
111+
/// doesn't want this new feature.
112+
/// </para>
113+
/// <para>
114+
/// So although we can't use a value type with <c>in</c> in public APIs dues to constraints
115+
/// on expression trees, we can do so internally. This type is a value-typed version of
116+
/// <see cref="TaskObservationOptions"/> enabling us to share code paths without forcing
117+
/// new allocations on existing code.
118+
/// </para>
119+
/// </remarks>
120+
internal readonly struct Value
121+
{
122+
internal Value(IScheduler? scheduler, bool ignoreExceptionsAfterUnsubscribe)
123+
{
124+
Scheduler = scheduler;
125+
IgnoreExceptionsAfterUnsubscribe = ignoreExceptionsAfterUnsubscribe;
126+
}
127+
128+
public IScheduler? Scheduler { get; }
129+
public bool IgnoreExceptionsAfterUnsubscribe { get; }
130+
}
131+
}
132+
}

Rx.NET/Source/src/System.Reactive/Linq/IQueryLanguage.cs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -195,25 +195,25 @@ internal partial interface IQueryLanguage
195195

196196
IObservable<TSource> StartAsync<TSource>(Func<Task<TSource>> functionAsync);
197197
IObservable<TSource> StartAsync<TSource>(Func<CancellationToken, Task<TSource>> functionAsync);
198-
IObservable<TSource> StartAsync<TSource>(Func<Task<TSource>> functionAsync, IScheduler scheduler);
199-
IObservable<TSource> StartAsync<TSource>(Func<CancellationToken, Task<TSource>> functionAsync, IScheduler scheduler);
198+
IObservable<TSource> StartAsync<TSource>(Func<Task<TSource>> functionAsync, in TaskObservationOptions.Value options);
199+
IObservable<TSource> StartAsync<TSource>(Func<CancellationToken, Task<TSource>> functionAsync, in TaskObservationOptions.Value options);
200200

201201
IObservable<Unit> Start(Action action);
202202
IObservable<Unit> Start(Action action, IScheduler scheduler);
203203

204204
IObservable<Unit> StartAsync(Func<Task> actionAsync);
205205
IObservable<Unit> StartAsync(Func<CancellationToken, Task> actionAsync);
206-
IObservable<Unit> StartAsync(Func<Task> actionAsync, IScheduler scheduler);
207-
IObservable<Unit> StartAsync(Func<CancellationToken, Task> actionAsync, IScheduler scheduler);
206+
IObservable<Unit> StartAsync(Func<Task> actionAsync, in TaskObservationOptions.Value options);
207+
IObservable<Unit> StartAsync(Func<CancellationToken, Task> actionAsync, in TaskObservationOptions.Value options);
208208

209209
IObservable<TResult> FromAsync<TResult>(Func<Task<TResult>> functionAsync);
210210
IObservable<TResult> FromAsync<TResult>(Func<CancellationToken, Task<TResult>> functionAsync);
211211
IObservable<Unit> FromAsync(Func<Task> actionAsync);
212212
IObservable<Unit> FromAsync(Func<CancellationToken, Task> actionAsync);
213-
IObservable<TResult> FromAsync<TResult>(Func<Task<TResult>> functionAsync, IScheduler scheduler);
214-
IObservable<TResult> FromAsync<TResult>(Func<CancellationToken, Task<TResult>> functionAsync, IScheduler scheduler);
215-
IObservable<Unit> FromAsync(Func<Task> actionAsync, IScheduler scheduler);
216-
IObservable<Unit> FromAsync(Func<CancellationToken, Task> actionAsync, IScheduler scheduler);
213+
IObservable<TResult> FromAsync<TResult>(Func<Task<TResult>> functionAsync, TaskObservationOptions.Value options);
214+
IObservable<TResult> FromAsync<TResult>(Func<CancellationToken, Task<TResult>> functionAsync, TaskObservationOptions.Value options);
215+
IObservable<Unit> FromAsync(Func<Task> actionAsync, TaskObservationOptions.Value options);
216+
IObservable<Unit> FromAsync(Func<CancellationToken, Task> actionAsync, TaskObservationOptions.Value options);
217217

218218
Func<IObservable<TResult>> ToAsync<TResult>(Func<TResult> function);
219219
Func<IObservable<TResult>> ToAsync<TResult>(Func<TResult> function, IScheduler scheduler);
@@ -398,8 +398,8 @@ internal partial interface IQueryLanguage
398398

399399
IObservable<TValue> Defer<TValue>(Func<IObservable<TValue>> observableFactory);
400400

401-
IObservable<TValue> Defer<TValue>(Func<Task<IObservable<TValue>>> observableFactoryAsync);
402-
IObservable<TValue> Defer<TValue>(Func<CancellationToken, Task<IObservable<TValue>>> observableFactoryAsync);
401+
IObservable<TValue> Defer<TValue>(Func<Task<IObservable<TValue>>> observableFactoryAsync, bool ignoreExceptionsAfterUnsubscribe);
402+
IObservable<TValue> Defer<TValue>(Func<CancellationToken, Task<IObservable<TValue>>> observableFactoryAsync, bool ignoreExceptionsAfterUnsubscribe);
403403

404404
IObservable<TResult> Empty<TResult>();
405405
IObservable<TResult> Empty<TResult>(IScheduler scheduler);

0 commit comments

Comments
 (0)