TaskErrorObservation.cs 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  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. using System;
  5. using System.Collections.Generic;
  6. using System.Linq;
  7. using System.Runtime.CompilerServices;
  8. using System.Text;
  9. using System.Threading;
  10. using System.Threading.Tasks;
  11. using Xunit;
  12. namespace Tests.System.Reactive
  13. {
  14. /// <summary>
  15. /// Verifies behavior around unobserved exceptions from tasks.
  16. /// </summary>
  17. /// <remarks>
  18. /// Testing whether unhandled exceptions emerge from <see cref="TaskScheduler.UnobservedTaskException"/> is not
  19. /// entirely straightforward. A few tests need to do this because we have some historical behaviour described in
  20. /// https://github.com/dotnet/reactive/issues/1256 that needs to be preserved for backwards compatibility, along
  21. /// with some new functionality enabling optional different behavior regarding unobserved exceptions. This provides
  22. /// common mechanism to enable such testing.
  23. /// </remarks>
  24. internal class TaskErrorObservation : IDisposable
  25. {
  26. private ManualResetEventSlim _exceptionReportedAsUnobserved;
  27. private WeakReference<Task> _taskWeakReference;
  28. public TaskErrorObservation()
  29. {
  30. _exceptionReportedAsUnobserved = new(false);
  31. TaskScheduler.UnobservedTaskException += HandleUnobservedException;
  32. }
  33. public Exception Exception { get; } = new();
  34. public void Dispose()
  35. {
  36. if (_exceptionReportedAsUnobserved is not null)
  37. {
  38. _exceptionReportedAsUnobserved.Dispose();
  39. _exceptionReportedAsUnobserved = null;
  40. TaskScheduler.UnobservedTaskException -= HandleUnobservedException;
  41. }
  42. }
  43. [MethodImpl(MethodImplOptions.NoInlining)]
  44. public IDisposable SuscribeWithoutKeepingSourceReachable(
  45. Func<Func<Task, Task>, Exception, IDisposable> subscribe)
  46. {
  47. // We provide nested function because the temporary storage location that
  48. // holds the value returned by a call to, say, Observable.StartAsync can end up keeping it reachable
  49. // for GC purposes, which in turn keeps the task reachable. That stops the
  50. // finalization-driven unobserved exception detection from working.
  51. // By calling Subscribe in a method whose stack frame is then immediately torn
  52. // down, we ensure that we don't hang onto anything other than the IDisposable
  53. // it returns.
  54. return subscribe(
  55. t =>
  56. {
  57. _taskWeakReference = new(t);
  58. return t;
  59. },
  60. Exception);
  61. }
  62. public IDisposable SuscribeWithoutKeepingSourceReachable<T>(
  63. Func<Func<Task<T>, Task<T>>, Exception, IDisposable> subscribe)
  64. {
  65. #pragma warning disable IDE0350 // Use implicitly typed lambda - we want to be explicit here for clarity
  66. return SuscribeWithoutKeepingSourceReachable(
  67. (Func<Task, Task> setTask, Exception ex) => subscribe(
  68. t =>
  69. {
  70. setTask(t);
  71. return t;
  72. }, ex));
  73. #pragma warning restore IDE0350
  74. }
  75. public void AssertExceptionReportedAsUnobserved()
  76. {
  77. var start = Environment.TickCount;
  78. var firstIteration = true;
  79. while (!_exceptionReportedAsUnobserved.Wait(TimeSpan.FromSeconds(firstIteration ? 0 : 0.001)) &&
  80. ((Environment.TickCount - start) < 5000))
  81. {
  82. firstIteration = false;
  83. GC.Collect();
  84. GC.WaitForPendingFinalizers();
  85. }
  86. Assert.True(_exceptionReportedAsUnobserved.Wait(TimeSpan.FromSeconds(0.01)));
  87. }
  88. /// <summary>
  89. /// Waits for the task to become unreachable, and then verifies that this did not result in
  90. /// <see cref="TaskScheduler.UnobservedTaskException"/> reporting the failure.
  91. /// </summary>
  92. /// <exception cref="InvalidOperationException"></exception>
  93. public void AssertExceptionNotReportedAsUnobserved()
  94. {
  95. if (_taskWeakReference is null)
  96. {
  97. throw new InvalidOperationException("Test did not supply task to " + nameof(TaskErrorObservation));
  98. }
  99. var start = Environment.TickCount;
  100. var firstIteration = true;
  101. do
  102. {
  103. // We try to get away without sleeping, to enable tests to run as quickly as
  104. // possible, but if the object remains reachable after the initial attempt to
  105. // force a GC and then immediately run finalizers, there's probably some deferred
  106. // work waiting to happen somewhere, so we are better off backing off and giving
  107. // that a chance to run.
  108. if (firstIteration)
  109. {
  110. firstIteration = false;
  111. }
  112. else
  113. {
  114. Thread.Sleep(1);
  115. }
  116. GC.Collect();
  117. GC.WaitForPendingFinalizers();
  118. } while (IsTaskStillReachable() &&
  119. ((Environment.TickCount - start) < 5000));
  120. // The task is now unreachable, but it's possible that this happened in between our
  121. // last call to GC.WaitForPendingFinalizers and our test for reachability, in which
  122. // case it might still be awaiting finalization, so we need one more of these to ensure
  123. // it gets flushed through:
  124. GC.WaitForPendingFinalizers();
  125. Assert.False(_exceptionReportedAsUnobserved.IsSet);
  126. }
  127. // This needs to be done in a separate method to ensure that when the weak reference returns a task, we
  128. // immediately destroy the stack frame containing the temporary variable into which it was returned,
  129. // to avoid keeping the task reachable by accident.
  130. [MethodImpl(MethodImplOptions.NoInlining)]
  131. private bool IsTaskStillReachable()
  132. {
  133. return _taskWeakReference.TryGetTarget(out _);
  134. }
  135. private void HandleUnobservedException(object sender, UnobservedTaskExceptionEventArgs e)
  136. {
  137. if (e.Exception.InnerException == Exception)
  138. {
  139. e.SetObserved();
  140. _exceptionReportedAsUnobserved.Set();
  141. }
  142. }
  143. }
  144. }