如何在IObservable管道中保存异常并在最后重新抛出它?

时间:2015-07-30 23:45:58

标签: c# system.reactive

我有以下方法:

public IObservable<DataManagementWorkItem> GetWorkItemSource(int maxConcurrentCalls)
{
    return m_namespaceManager
        .GetNamespaceConnectionInfoSource(true, drainAndDisable: false)
        .Select(nci => Observable.Defer(() => GetPolicySourceForNamespace(nci)))
        .Merge(maxConcurrentCalls)
        .Where(IsValid)
        .Select(ToWorkItem)
        .Where(o => o != null);
}

它实现了以下逻辑:

  1. 通过从命名空间管理器(IObservable<NamespaceConnectionInfo>)获取GetNamespaceConnectionInfoSource来输入monad。
  2. 当命名空间可用时,获取与特定命名空间(IObservable<DataManagementPolicy>)对应的GetPolicySourceForNamespace。但是,请使用Merge运算符将并发调用数限制为GetPolicySourceForNamespace
  3. 过滤掉错误的DataManagementPolicy记录(无法在SQL中完成)。
  4. 将看似良好的DataManagementPolicy记录翻译为DataManagementWorkItem个实例。有些可能会显示为null,因此最终会将其过滤掉。
  5. GetNamespaceConnectionInfoSource在生成一定数量的有效NamespaceConnectionInfo对象后可能会出错。完全有可能在最终可观察序列中已经生成了一定数量的DataManagementWorkItem个对象。

    我有一个单元测试,其中:

      生成25个名称空间后
    • GetNamespaceConnectionInfoSource抛出
    • GetPolicySourceForNamespace每个命名空间生成10个对象
    • 并发限制为10

    我也有兴趣检查最终观察中出现的项目是否有问题:

    var dm = DependencyResolver.Instance.GetInstance<IDataManagement>();
    var workItems = new List<DataManagementWorkItem>();
    try
    {
        var obs = dm.GetWorkItemSource(10);
        obs.Subscribe(wi => workItems.Add(wi));
        await obs;
        Assert.Fail("An expected exception was not thrown");
    }
    catch (Exception exc)
    {
        AssertTheRightException(exc);
    }
    

    workItems集合每次都有不同数量的项目。一次运行它有69个项目,另一个 - 50,另一个 - 18。

    我的解释是,当故障发生时,处理的各个阶段都有好的NamespaceConnectionInfoDataManagementPolicy个对象,所有这些对象都因故障而中止。每次的金额都不同,因为这些项是异步生成的。

    这就是我的问题 - 我不希望它们被中止。我希望它们运行完成,在最终的可观察序列中生成,然后才传达故障。本质上我想保留异常并在最后重新抛出它。

    我尝试稍微修改一下实现:

    public IObservable<DataManagementWorkItem> GetWorkItemSource(int maxConcurrentCalls)
    {
        Exception fault = null;
        return m_namespaceManager
            .GetNamespaceConnectionInfoSource(true, drainAndDisable: false)
            .Catch<NamespaceConnectionInfo, Exception>(exc =>
            {
                fault = exc;
                return Observable.Empty<NamespaceConnectionInfo>();
            })
            .Select(nci => Observable.Defer(() => GetPolicySourceForNamespace(nci)))
            .Merge(maxConcurrentCalls)
            .Where(IsValid)
            .Select(ToWorkItem)
            .Where(o => o != null)
            .Finally(() =>
            {
                if (fault != null)
                {
                    throw fault;
                }
            });
    }
    

    毋庸置疑 - 它没有用。 Finally似乎没有传播任何例外情况,我实际上同意这一点。

    那么,实现我想要的正确方法是什么?

    修改

    与此问题无关,我发现用于收集生成的DataManagementWorkItem实例的测试代码很糟糕。而不是

        var obs = dm.GetWorkItemSource(10);
        obs.Subscribe(wi => workItems.Add(wi));
        await obs;
    

    应该是

        await dm.GetWorkItemSource(1).Do(wi => workItems.Add(wi));
    

    不同之处在于,后者只订阅了一次项目来源,而原始版本订阅了两次:

    1. Subscribe
    2. await
    3. 它不会影响qustion,但会破坏我的模拟代码。

      澄清

      这更多的澄清。每个命名空间生成一系列10个策略对象。但是这个过程是异步的 - 策略对象是按顺序生成的,但是是异步生成的。在所有这段时间内,名称空间继续产生,因此在故障之前有25个命名空间,有三个可能的“状态”,生成的命名空间可以是:

      • 尚未为其生成任何策略对象,但异步策略生产过程已启动
      • 已经生成了一些(但少于10个)策略对象
      • 已生成名称空间的所有10个策略对象

      当命名空间生成中出现错误时,整个管道都将中止,而不管“好”命名空间现在处于什么状态。

      让我们看一下以下简单的例子:

      using System;
      using System.Collections.Generic;
      using System.Diagnostics;
      using System.Reactive.Linq;
      using System.Reactive.Subjects;
      using System.Threading;
      
      namespace observables
      {
          class Program
          {
              static void Main()
              {
                  int count = 0;
                  var obs = Observable
                      .Interval(TimeSpan.FromMilliseconds(1))
                      .Take(50)
                      .Select(i =>
                      {
                          if (25 == Interlocked.Increment(ref count))
                          {
                              throw new Exception("Boom!");
                          }
                          return i;
                      })
                      .Select(i => Observable.Defer(() => Observable.Interval(TimeSpan.FromMilliseconds(1)).Take(10).Select(j => i * 1000 + j)))
                      .Merge(10);
      
                  var items = new HashSet<long>();
                  try
                  {
                      obs.Do(i => items.Add(i)).GetAwaiter().GetResult();
                  }
                  catch (Exception exc)
                  {
                      Debug.WriteLine(exc.Message);
                  }
                  Debug.WriteLine(items.Count);
              }
          }
      }
      

      当我运行它时,我通常有以下输出:

      Boom!
      192
      

      但是,它也可以显示191.但是,如果我们应用故障concat解决方案(即使它在没有故障时不起作用):

              int count = 0;
              var fault = new Subject<long>();
              var obs = Observable
                  .Interval(TimeSpan.FromMilliseconds(1))
                  .Take(50)
                  .Select(i =>
                  {
                      if (25 == Interlocked.Increment(ref count))
                      {
                          throw new Exception("Boom!");
                      }
                      return i;
                  })
                  .Catch<long, Exception>(exc =>
                  {
                      fault.OnError(exc);
                      return Observable.Empty<long>();
                  })
                  .Select(i => Observable.Defer(() => Observable.Interval(TimeSpan.FromMilliseconds(1)).Take(10).Select(j => i * 1000 + j)))
                  .Merge(10)
                  .Concat(fault);
      

      然后输出始终为240,因为我们让所有已经开始的异步进程完成。

      基于pmccloghrylaing答案的尴尬解决方案

          public IObservable<DataManagementWorkItem> GetWorkItemSource(int maxConcurrentCalls)
          {
              var fault = new Subject<DataManagementWorkItem>();
              bool faulted = false;
              return m_namespaceManager
                  .GetNamespaceConnectionInfoSource(true, drainAndDisable: false)
                  .Catch<NamespaceConnectionInfo, Exception>(exc =>
                  {
                      faulted = true;
                      return Observable.Throw<NamespaceConnectionInfo>(exc);
                  })
                  .Finally(() =>
                  {
                      if (!faulted)
                      {
                          fault.OnCompleted();
                      }
                  })
                  .Catch<NamespaceConnectionInfo, Exception>(exc =>
                  {
                      fault.OnError(exc);
                      return Observable.Empty<NamespaceConnectionInfo>();
                  })
                  .Select(nci => Observable.Defer(() => GetPolicySourceForNamespace(nci)))
                  .Merge(maxConcurrentCalls)
                  .Where(IsValid)
                  .Select(ToWorkItem)
                  .Where(o => o != null)
                  .Concat(fault);
          }
      

      它既可以在命名空间生成出现故障时也可以在成功时出现,但看起来很尴尬。加上多个订阅仍然共享错误。必须有一个更优雅的解决方案。

      GetNamespaceConnectionInfoSource源代码

      public IObservable<NamespaceConnectionInfo> GetNamespaceConnectionInfoSource(bool? isActive = null,
          bool? isWorkflowEnabled = null, bool? isScheduleEnabled = null, bool? drainAndDisable = null,
          IEnumerable<string> nsList = null, string @where = null, IList<SqlParameter> whereParameters = null)
      {
          IList<SqlParameter> parameters;
          var sql = GetNamespaceConnectionInfoSqls.GetSql(isActive,
              isWorkflowEnabled, isScheduleEnabled, drainAndDisable, nsList, @where, whereParameters, out parameters);
          var sqlUtil = m_sqlUtilProvider.Get(m_siteSettings.ControlDatabaseConnString);
          return sqlUtil.GetSource(typeof(NamespaceConnectionInfo), sqlUtil.GetReaderAsync(sql, parameters)).Cast<NamespaceConnectionInfo>();
      }
      
      public IObservable<DbDataReader> GetReaderAsync(string query, IList<SqlParameter> parameters = null, CommandBehavior commandBehavior = CommandBehavior.Default)
      {
          return Observable.FromAsync(async () =>
          {
              SqlCommand command = null;
              try
              {
                  var conn = await GetConnectionAsync();
                  command = GetCommand(conn, query, parameters);
                  return (DbDataReader)await command.ExecuteReaderAsync(commandBehavior | CommandBehavior.CloseConnection);
              }
              finally
              {
                  DisposeSilently(command);
              }
          });
      }
      
      public IObservable<object> GetSource(Type objectType, IObservable<DbDataReader> readerTask)
      {
          return Observable.Create<object>(async (obs, ct) => await PopulateSource(objectType, await readerTask, true, obs, ct));
      }
      
      private static async Task PopulateSource(Type objectType, DbDataReader reader, bool disposeReader, IObserver<object> obs, CancellationToken ct)
      {
          try
          {
              if (IsPrimitiveDataType(objectType))
              {
                  while (await reader.ReadAsync(ct))
                  {
                      obs.OnNext(reader[0]);
                  }
              }
              else
              {
                  // Get all the properties in our Object
                  var typeReflector = objectType.GetTypeReflector(TypeReflectorCreationStrategy.PREPARE_DATA_RECORD_CONSTRUCTOR);
      
                  // For each property get the data from the reader to the object
                  while (await reader.ReadAsync(ct))
                  {
                      obs.OnNext(typeReflector.DataRecordConstructor == null ?
                          ReadNextObject(typeReflector, reader) :
                          typeReflector.DataRecordConstructor(reader));
                  }
              }
          }
          catch (OperationCanceledException)
          {
          }
          finally
          {
              if (disposeReader)
              {
                  reader.Dispose();
              }
          }
      }
      

3 个答案:

答案 0 :(得分:1)

m_namespaceManager.GetNamespaceConnectionInfoSource(true, drainAndDisable: false)的调用会返回IObservable<NamespaceConnectionInfo>。现在,任何一个可观察的合同都是这样的:

OnNext*(OnError|OnCompleted)

这意味着您获得零个或多个值,后跟一个错误或完成中的一个,而且只有一个。

您无法从单个observable中获取多个错误,并且在收到错误后无法获取值。

如果你的observable确实返回了多个错误,那就违反了正常的Rx合约。

因此,鉴于此,鉴于现有代码,您不可能将错误延迟到observable结束,因为错误 是observable的结尾。

您可以做的是更改GetNamespaceConnectionInfoSource中生成值的方式,以便在将它们合并为一个之前生成多个序列调用.Materialize()。这意味着您将拥有一个IObservable<Notification<NamespaceConnectionInfo>>,并且可以在整个流中包含多个错误和完成。然后,您可以在处理错误之前对此流进行分组并处理这些值。但这一切都取决于对GetNamespaceConnectionInfoSource的更改,并且由于您尚未发布此消息来源,因此我无法为您提供正确的代码。

要帮助理解这一点,请查看以下代码:

var xs = new [] { 1, 2, 3, 0, 4, 0, 5 }.ToObservable();

xs
    .Select(x =>
    {
        if (x == 0)
            throw new NotSupportedException();
        else
            return x;
    })
    .Subscribe(
        x => Console.WriteLine(x),
        ex => Console.WriteLine(ex.ToString()));

它产生了这个:

1
2
3
System.NotSupportedException: Specified method is not supported.
   at UserQuery.<Main>b__0(Int32 x) in query_ioaahp.cs:line 45
   at System.Reactive.Linq.ObservableImpl.Select`2._.OnNext(TSource value)

4&amp; 5根本不会产生。

现在看看这段代码:

xs
    .Select(x =>
        Observable
            .Start(() =>
            {
                if (x == 0)
                    throw new NotSupportedException();
                else
                    return x;
            })
            .Materialize())
    .Merge()
    .Where(x => x.Kind != NotificationKind.OnCompleted)
    .Subscribe(
        x => Console.WriteLine(String.Format(
            "{0} {1}",
            x.Kind,
            x.HasValue ? x.Value.ToString() : "")),
        ex => Console.WriteLine(ex.ToString()));

这会产生以下结果:

OnNext 1
OnNext 4
OnError 
OnError 
OnNext 5
OnNext 3
OnNext 2

由于引入了并行性,它出了故障。

但现在你可以处理所有的错误了。

答案 1 :(得分:0)

Concat会解决您的问题吗?我已将Observable.CreateFinally一起打包,以完成faults主题。

public IObservable<DataManagementWorkItem> GetWorkItemSource(int maxConcurrentCalls)
{
    return Observable.Create<DataManagementWorkItem>((observer) =>
    {
        var faults = new Subject<DataManagementWorkItem>();
        return m_namespaceManager
            .GetNamespaceConnectionInfoSource(true, drainAndDisable: false)
            .Catch<NamespaceConnectionInfo, Exception>(exc =>
            {
                faults.OnError(exc);
                return Observable.Empty<NamespaceConnectionInfo>();
            })
            .Take(maxConcurrentCalls)
            .Select(nci => GetPolicySourceForNamespace(nci))
            .Merge()
            .Where(IsValid)
            .Select(ToWorkItem)
            .Where(o => o != null)
            .Finally(() => faults.OnCompleted())
            .Concat(faults)
            .Subscribe(observer);
    });
}

另外,这会回归你的期望吗? (在你的测试中24)

m_namespaceManager
    .GetNamespaceConnectionInfoSource(true, drainAndDisable: false)
    .Catch<NamespaceConnectionInfo, Exception>(exc =>
    {
        faults.OnError(exc);
        return Observable.Empty<NamespaceConnectionInfo>();
    })
    .Count()

答案 2 :(得分:0)

是的,基本问题是Merge快速实施失败。如果source observable产生错误,或者任何内部observable产生错误,那么Merge将使流失败而不等待剩余的内部observable完成。

为了达到你想要的效果,你需要在合并之前“捕获”错误,并在内部observable完成后“重新抛出”它:

public IObservable<DataManagementWorkItem> GetWorkItemSource(int maxConcurrentCalls)
{
    // wrap within Observable.Defer
    // so that each new subscription
    // gets its own Error subject
    return Observable.Defer(() =>
    {
        var error = new ReplaySubject<DataManagementWorkItem>(1);

        return m_namespaceManager
            .GetNamespaceConnectionInfoSource(true, drainAndDisable: false)
            .Catch(err =>
            {
                error.OnError(err);
                return Observable.Empty<NamespaceConnectionInfo>();
            })
            .Finally(error.OnCompleted)
            .Select(nci => Observable.Defer(() => GetPolicySourceForNamespace(nci)))
            .Merge(maxConcurrentCalls)
            .Where(IsValid)
            .Select(ToWorkItem)
            .Where(o => o != null)
            .Concat(error);
    });
}

另外,我注意到你的单元测试是在返回的observable中订阅了两次,这增加了你的困惑。致电Subscribe以填充您的列表,然后再次使用await。你真的只想订阅一次。我们可以使用.Do运算符填充您的列表,您应该能够在错误处理程序中检查它:

var dm = DependencyResolver.Instance.GetInstance<IDataManagement>();
var workItems = new List<DataManagementWorkItem>();
try
{
    var obs = dm.GetWorkItemSource(10).Do(workItems.Add);
    await obs;
    Assert.Fail("An expected exception was not thrown");
}
catch (Exception exc)
{
    AssertTheRightException(exc);
    // workItems should be populated.
}