如何正确包装SqlDataReader和IObservable?

时间:2014-05-24 16:43:30

标签: c# multithreading asynchronous system.reactive sqldatareader

我想探讨使用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 - 如果消耗很快就会阻塞,如果消耗很慢,它会在内部队列中实现数据,但这种情况仍是合法的。
  • 最后人们可以通过订阅直接使用observable,但是他们必须在某个时候等待结束(阻塞线程),因为大多数代码仍然是同步的。

基本要求是,一旦读数结束,数据阅读器就会被迅速处理掉 - 无论观察者的消费方式如何。

在所有单元测试中4失败:

  • SubscribeCancelSubscribeThrow超时(即死锁)
  • ToEnumerableForEachBreakToEnumerableForEachThrow无法验证数据阅读器处理。

数据读取器处理验证失败是一个时间问题 - 当剩下foreach时(通过异常或中断),相应的IEnumerator会立即被释放,这最终取消了使用的取消令牌。实施可观察的。但是,该实现在另一个线程上运行,并且当它注意到取消时 - 单元测试已经结束。在实际应用中,读者将被正确且相当迅速地处理掉,但它与迭代的结束不同步。我想知道是否有可能处理上述IEnumerator实例,等待相应的IObservable实现注意到取消并且读者被处置。

修改

所以DbDataReaderIEnumerable,这意味着如果有人希望同步枚举对象 - 没问题。

但是,如果我想异步执行该怎么办?在这种情况下,我被禁止列举读者 - 这是一个阻塞操作。唯一的出路是返回一个可观察的。其他人已经用比以往更好的语言讨论了这个主题,例如 - http://www.interact-sw.co.uk/iangblog/2013/11/29/async-yield-return

因此我必须返回IObservable,我不能使用ToObservable扩展方法,因为它取决于读者的阻塞枚举。

接下来,鉴于IObservable有人可能将其转换为IEnumerable,这是愚蠢的,因为读者已经是IEnumerable,但仍然可行且合法。< / p>

编辑2

使用.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处理程序。但事实并非如此。

编辑3

关注Why is the OnError callback never called when throwing from the given subscriber?我现在想知道应该采用什么方法来实现以下目标:

  1. 通过异步读取SqlDataReader实例
  2. 公开可用对象
  3. 避免物化的实现。具体化的选择应该由API的调用者掌握。
  4. API应该可以在异步代码与同步混合的环境中使用。为什么?因为我们已经有一台使用同步IO的服务器,我们希望逐步淘汰同步阻塞IO和异步IO。
  5. 在我面前有这些目标,我想出了类似的东西(参见单元测试代码):

    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单元测试代码所示。但是,SubcribeCancelSubscribeThrow的结果表明此使用模式是错误的。 Why is the OnError callback never called when throwing from the given subscriber?解释了为什么这是错误的。

    那么,正确的方法是什么?如何防止API的使用者错误地使用它(SubcribeCancelSubscribeThrow是这种不正确消费的例子。)

1 个答案:

答案 0 :(得分:0)

SubscribeCancel

由于SubscribeCancel取消,

cts失败。这不会调用OnError处理程序。

取消cts与处理您的订阅同义。处置订阅会导致所有未来的OnNextOnErrorOnCompleted来电被忽略。因此,任务永远不会完成,测试将永远挂起。

<强>解决方案:

取消cts时,将任务设置为正确的状态。

SubscribeThrow

SubscribeThrow由于OnNext处理程序中的异常而失败。

OnNext处理程序中抛出异常不会将异常转发给OnError处理程序。

<强>解决方案:

不要在Subscribe处理程序中抛出异常。而是处理您的订阅并将Task设置为正确的状态。

ToEnumerableForEachThrow&amp; ToEnumerableForEachBreak

由于竞争条件,

ToEnumerableForEachThrowToEnumerableForEachBreak失败。

枚举上的

foreach(...)将在底层的observable上调用dispose,这将取消取消令牌。在那之后,你的测试捕获(或休息只是退出foreach)捕获异常,在那里你测试看到底层读者被处理......除了读者还没有被处理掉,因为observable仍在等待读者产生下一个结果......只有在读者产生(和可观察的收益率)之后,可观察的循环才会回来并检查取消令牌。此时,observable断开并退出使用块并处理读取器。

<强>解决方案:

而不是using (...)语句,请从Disposable返回Observable.Create。一次性将在订购时处理。这就是你想要的。一起摆脱using声明,让Rx做好工作。