TaskErrorObservation.cs 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  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. return SuscribeWithoutKeepingSourceReachable(
  66. (Func<Task, Task> setTask, Exception ex) => subscribe(
  67. t =>
  68. {
  69. setTask(t);
  70. return t;
  71. }, ex));
  72. }
  73. public void AssertExceptionReportedAsUnobserved()
  74. {
  75. var start = Environment.TickCount;
  76. var firstIteration = true;
  77. while (!_exceptionReportedAsUnobserved.Wait(TimeSpan.FromSeconds(firstIteration ? 0 : 0.001)) &&
  78. ((Environment.TickCount - start) < 5000))
  79. {
  80. firstIteration = false;
  81. GC.Collect();
  82. GC.WaitForPendingFinalizers();
  83. }
  84. Assert.True(_exceptionReportedAsUnobserved.Wait(TimeSpan.FromSeconds(0.01)));
  85. }
  86. /// <summary>
  87. /// Waits for the task to become unreachable, and then verifies that this did not result in
  88. /// <see cref="TaskScheduler.UnobservedTaskException"/> reporting the failure.
  89. /// </summary>
  90. /// <exception cref="InvalidOperationException"></exception>
  91. public void AssertExceptionNotReportedAsUnobserved()
  92. {
  93. if (_taskWeakReference is null)
  94. {
  95. throw new InvalidOperationException("Test did not supply task to " + nameof(TaskErrorObservation));
  96. }
  97. var start = Environment.TickCount;
  98. var firstIteration = true;
  99. do
  100. {
  101. // We try to get away without sleeping, to enable tests to run as quickly as
  102. // possible, but if the object remains reachable after the initial attempt to
  103. // force a GC and then immediately run finalizers, there's probably some deferred
  104. // work waiting to happen somewhere, so we are better off backing off and giving
  105. // that a chance to run.
  106. if (firstIteration)
  107. {
  108. firstIteration = false;
  109. }
  110. else
  111. {
  112. Thread.Sleep(1);
  113. }
  114. GC.Collect();
  115. GC.WaitForPendingFinalizers();
  116. } while (IsTaskStillReachable() &&
  117. ((Environment.TickCount - start) < 5000));
  118. // The task is now unreachable, but it's possible that this happened in between our
  119. // last call to GC.WaitForPendingFinalizers and our test for reachability, in which
  120. // case it might still be awaiting finalization, so we need one more of these to ensure
  121. // it gets flushed through:
  122. GC.WaitForPendingFinalizers();
  123. Assert.False(_exceptionReportedAsUnobserved.IsSet);
  124. }
  125. // This needs to be done in a separate method to ensure that when the weak reference returns a task, we
  126. // immediately destroy the stack frame containing the temporary variable into which it was returned,
  127. // to avoid keeping the task reachable by accident.
  128. [MethodImpl(MethodImplOptions.NoInlining)]
  129. private bool IsTaskStillReachable()
  130. {
  131. return _taskWeakReference.TryGetTarget(out _);
  132. }
  133. private void HandleUnobservedException(object sender, UnobservedTaskExceptionEventArgs e)
  134. {
  135. if (e.Exception.InnerException == Exception)
  136. {
  137. e.SetObserved();
  138. _exceptionReportedAsUnobserved.Set();
  139. }
  140. }
  141. }
  142. }