我想探讨使用IObservable<T>
作为SqlDataReader
的包装器的可能性。到目前为止,我们使用阅读器来避免在内存中实现整个结果,我们使用阻塞同步API。
现在我们想尝试将异步API与.NET Reactive Extensions结合使用。
但是,这段代码必须与同步代码共存,因为采用异步方式是一个渐进的过程。
我们已经知道,这种同步和异步的混合在ASP.NET中不起作用 - 因为整个请求执行路径必须始终是异步的。关于这一主题的优秀文章是http://blog.stephencleary.com/2012/07/dont-block-on-async-code.html
但我说的是一个简单的WCF服务。我们已经在那里混合了异步和同步代码,但这是我们第一次引入Rx并且存在问题。
我创建了简单的单元测试(我们使用mstest,叹气:-()来演示问题。我希望有人能够解释我发生了什么。请在下面找到完整的源代码(使用MOQ):
using System;
using System.Data.Common;
using System.Diagnostics;
using System.Linq;
using System.Reactive.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
namespace UnitTests
{
public static class Extensions
{
public static Task<List<T>> ToListAsync<T>(this IObservable<T> observable)
{
var res = new List<T>();
var tcs = new TaskCompletionSource<List<T>>();
observable.Subscribe(res.Add, e => tcs.TrySetException(e), () => tcs.TrySetResult(res));
return tcs.Task;
}
}
[TestClass]
public class TestRx
{
public const int UNIT_TEST_TIMEOUT = 5000;
private static DbDataReader CreateDataReader(int count = 100, int msWait = 10)
{
var curItemIndex = -1;
var mockDataReader = new Mock<DbDataReader>();
mockDataReader.Setup(o => o.ReadAsync(It.IsAny<CancellationToken>())).Returns<CancellationToken>(ct => Task.Factory.StartNew(() =>
{
Thread.Sleep(msWait);
if (curItemIndex + 1 < count && !ct.IsCancellationRequested)
{
++curItemIndex;
return true;
}
Trace.WriteLine(curItemIndex);
return false;
}));
mockDataReader.Setup(o => o[0]).Returns<int>(_ => curItemIndex);
mockDataReader.CallBase = true;
mockDataReader.Setup(o => o.Close()).Verifiable();
return mockDataReader.Object;
}
private static IObservable<int> GetObservable(DbDataReader reader)
{
return Observable.Create<int>(async (obs, cancellationToken) =>
{
using (reader)
{
while (!cancellationToken.IsCancellationRequested && await reader.ReadAsync(cancellationToken))
{
obs.OnNext((int)reader[0]);
}
}
});
}
[TestMethod, TestCategory("CI"), Timeout(UNIT_TEST_TIMEOUT)]
public void ToListAsyncResult()
{
var reader = CreateDataReader();
var numbers = GetObservable(reader).ToListAsync().Result;
CollectionAssert.AreEqual(Enumerable.Range(0, 100).ToList(), numbers);
Mock.Get(reader).Verify(o => o.Close());
}
[TestMethod, TestCategory("CI"), Timeout(UNIT_TEST_TIMEOUT)]
public void ToEnumerableToList()
{
var reader = CreateDataReader();
var numbers = GetObservable(reader).ToEnumerable().ToList();
CollectionAssert.AreEqual(Enumerable.Range(0, 100).ToList(), numbers);
Mock.Get(reader).Verify(o => o.Close());
}
[TestMethod, TestCategory("CI"), Timeout(UNIT_TEST_TIMEOUT)]
public void ToEnumerableForEach()
{
var reader = CreateDataReader();
int i = 0;
foreach (var n in GetObservable(reader).ToEnumerable())
{
Assert.AreEqual(i, n);
++i;
}
Assert.AreEqual(100, i);
Mock.Get(reader).Verify(o => o.Close());
}
[TestMethod, TestCategory("CI"), Timeout(UNIT_TEST_TIMEOUT)]
public void ToEnumerableForEachBreak()
{
var reader = CreateDataReader();
int i = 0;
foreach (var n in GetObservable(reader).ToEnumerable())
{
Assert.AreEqual(i, n);
++i;
if (i == 5)
{
break;
}
}
Mock.Get(reader).Verify(o => o.Close());
}
[TestMethod, TestCategory("CI"), Timeout(UNIT_TEST_TIMEOUT)]
public void ToEnumerableForEachThrow()
{
var reader = CreateDataReader();
int i = 0;
try
{
foreach (var n in GetObservable(reader).ToEnumerable())
{
Assert.AreEqual(i, n);
++i;
if (i == 5)
{
throw new Exception("xo-xo");
}
}
Assert.Fail();
}
catch (Exception exc)
{
Assert.AreEqual("xo-xo", exc.Message);
Mock.Get(reader).Verify(o => o.Close());
}
}
[TestMethod, TestCategory("CI"), Timeout(UNIT_TEST_TIMEOUT)]
public void Subscribe()
{
var reader = CreateDataReader();
var tcs = new TaskCompletionSource<object>();
int i = 0;
GetObservable(reader).Subscribe(n =>
{
Assert.AreEqual(i, n);
++i;
}, () =>
{
Assert.AreEqual(100, i);
Mock.Get(reader).Verify(o => o.Close());
tcs.TrySetResult(null);
});
tcs.Task.Wait();
}
[TestMethod, TestCategory("CI"), Timeout(UNIT_TEST_TIMEOUT)]
public void SubscribeCancel()
{
var reader = CreateDataReader();
var tcs = new TaskCompletionSource<object>();
var cts = new CancellationTokenSource();
int i = 0;
GetObservable(reader).Subscribe(n =>
{
Assert.AreEqual(i, n);
++i;
if (i == 5)
{
cts.Cancel();
}
}, e =>
{
Assert.IsTrue(i < 100);
Mock.Get(reader).Verify(o => o.Close());
tcs.TrySetException(e);
}, () =>
{
Assert.IsTrue(i < 100);
Mock.Get(reader).Verify(o => o.Close());
tcs.TrySetResult(null);
}, cts.Token);
tcs.Task.Wait();
}
[TestMethod, TestCategory("CI"), Timeout(UNIT_TEST_TIMEOUT)]
public void SubscribeThrow()
{
var reader = CreateDataReader();
var tcs = new TaskCompletionSource<object>();
int i = 0;
GetObservable(reader).Subscribe(n =>
{
Assert.AreEqual(i, n);
++i;
if (i == 5)
{
throw new Exception("xo-xo");
}
}, e =>
{
Assert.AreEqual("xo-xo", e.Message);
Mock.Get(reader).Verify(o => o.Close());
tcs.TrySetResult(null);
});
tcs.Task.Wait();
}
}
}
这些单元测试捕获了返回IObservable<T>
包装数据读取器的API的所有可能用途:
ToListAsync
扩展方法或.ToEnumerable().ToList()
完全实现它。ToEnumerable
扩展方法对其进行迭代。 True - 如果消耗很快就会阻塞,如果消耗很慢,它会在内部队列中实现数据,但这种情况仍是合法的。基本要求是,一旦读数结束,数据阅读器就会被迅速处理掉 - 无论观察者的消费方式如何。
在所有单元测试中4失败:
SubscribeCancel
和SubscribeThrow
超时(即死锁)ToEnumerableForEachBreak
和ToEnumerableForEachThrow
无法验证数据阅读器处理。数据读取器处理验证失败是一个时间问题 - 当剩下foreach
时(通过异常或中断),相应的IEnumerator
会立即被释放,这最终取消了使用的取消令牌。实施可观察的。但是,该实现在另一个线程上运行,并且当它注意到取消时 - 单元测试已经结束。在实际应用中,读者将被正确且相当迅速地处理掉,但它与迭代的结束不同步。我想知道是否有可能处理上述IEnumerator
实例,等待相应的IObservable
实现注意到取消并且读者被处置。
所以DbDataReader
是IEnumerable
,这意味着如果有人希望同步枚举对象 - 没问题。
但是,如果我想异步执行该怎么办?在这种情况下,我被禁止列举读者 - 这是一个阻塞操作。唯一的出路是返回一个可观察的。其他人已经用比以往更好的语言讨论了这个主题,例如 - http://www.interact-sw.co.uk/iangblog/2013/11/29/async-yield-return
因此我必须返回IObservable
,我不能使用ToObservable
扩展方法,因为它取决于读者的阻塞枚举。
接下来,鉴于IObservable
有人可能将其转换为IEnumerable
,这是愚蠢的,因为读者已经是IEnumerable
,但仍然可行且合法。< / p>
使用.NET Reflector(与VS集成)调试代码显示流程通过以下方法:
namespace System.Reactive.Threading.Tasks
{
public static class TaskObservableExtensions
{
...
private static void ToObservableDone<TResult>(Task<TResult> task, AsyncSubject<TResult> subject)
{
switch (task.get_Status())
{
case TaskStatus.RanToCompletion:
subject.OnNext(task.get_Result());
subject.OnCompleted();
return;
case TaskStatus.Canceled:
subject.OnError((Exception) new TaskCanceledException((Task) task));
return;
case TaskStatus.Faulted:
subject.OnError(task.get_Exception().get_InnerException());
return;
}
}
}
}
取消令牌并从异步订阅中的OnNext
投掷都归入此方法(以及成功完成)。取消和抛出都会收敛到subject.OnError
方法。该方法应该最终委托给OnError
处理程序。但事实并非如此。
关注Why is the OnError callback never called when throwing from the given subscriber?我现在想知道应该采用什么方法来实现以下目标:
SqlDataReader
实例在我面前有这些目标,我想出了类似的东西(参见单元测试代码):
private static IObservable<int> GetObservable(DbDataReader reader)
{
return Observable.Create<int>(async (obs, cancellationToken) =>
{
using (reader)
{
while (!cancellationToken.IsCancellationRequested && await reader.ReadAsync(cancellationToken))
{
obs.OnNext((int)reader[0]);
}
}
});
}
对你有意义吗?如果没有,有哪些替代方案?
接下来,我正在考虑使用它,如Subscribe
单元测试代码所示。但是,SubcribeCancel
和SubscribeThrow
的结果表明此使用模式是错误的。 Why is the OnError callback never called when throwing from the given subscriber?解释了为什么这是错误的。
那么,正确的方法是什么?如何防止API的使用者错误地使用它(SubcribeCancel
和SubscribeThrow
是这种不正确消费的例子。)
答案 0 :(得分:0)
SubscribeCancel
取消, cts
失败。这不会调用OnError
处理程序。
取消cts
与处理您的订阅同义。处置订阅会导致所有未来的OnNext
,OnError
和OnCompleted
来电被忽略。因此,任务永远不会完成,测试将永远挂起。
<强>解决方案:强>
取消cts时,将任务设置为正确的状态。
SubscribeThrow
由于OnNext
处理程序中的异常而失败。
在OnNext
处理程序中抛出异常不会将异常转发给OnError
处理程序。
<强>解决方案:强>
不要在Subscribe
处理程序中抛出异常。而是处理您的订阅并将Task
设置为正确的状态。
ToEnumerableForEachThrow
和ToEnumerableForEachBreak
失败。
foreach(...)
将在底层的observable上调用dispose,这将取消取消令牌。在那之后,你的测试捕获(或休息只是退出foreach)捕获异常,在那里你测试看到底层读者被处理......除了读者还没有被处理掉,因为observable仍在等待读者产生下一个结果......只有在读者产生(和可观察的收益率)之后,可观察的循环才会回来并检查取消令牌。此时,observable断开并退出使用块并处理读取器。
<强>解决方案:强>
而不是using (...)
语句,请从Disposable
返回Observable.Create
。一次性将在订购时处理。这就是你想要的。一起摆脱using
声明,让Rx
做好工作。