我有以下方法:
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);
}
它实现了以下逻辑:
IObservable<NamespaceConnectionInfo>
)获取GetNamespaceConnectionInfoSource
来输入monad。IObservable<DataManagementPolicy>
)对应的GetPolicySourceForNamespace
。但是,请使用Merge
运算符将并发调用数限制为GetPolicySourceForNamespace
。DataManagementPolicy
记录(无法在SQL中完成)。DataManagementPolicy
记录翻译为DataManagementWorkItem
个实例。有些可能会显示为null
,因此最终会将其过滤掉。 GetNamespaceConnectionInfoSource
在生成一定数量的有效NamespaceConnectionInfo
对象后可能会出错。完全有可能在最终可观察序列中已经生成了一定数量的DataManagementWorkItem
个对象。
我有一个单元测试,其中:
GetNamespaceConnectionInfoSource
抛出GetPolicySourceForNamespace
每个命名空间生成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。
我的解释是,当故障发生时,处理的各个阶段都有好的NamespaceConnectionInfo
和DataManagementPolicy
个对象,所有这些对象都因故障而中止。每次的金额都不同,因为这些项是异步生成的。
这就是我的问题 - 我不希望它们被中止。我希望它们运行完成,在最终的可观察序列中生成,然后才传达故障。本质上我想保留异常并在最后重新抛出它。
我尝试稍微修改一下实现:
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));
不同之处在于,后者只订阅了一次项目来源,而原始版本订阅了两次:
Subscribe
await
它不会影响qustion,但会破坏我的模拟代码。
澄清
这更多的澄清。每个命名空间生成一系列10个策略对象。但是这个过程是异步的 - 策略对象是按顺序生成的,但是是异步生成的。在所有这段时间内,名称空间继续产生,因此在故障之前有25个命名空间,有三个可能的“状态”,生成的命名空间可以是:
当命名空间生成中出现错误时,整个管道都将中止,而不管“好”命名空间现在处于什么状态。
让我们看一下以下简单的例子:
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();
}
}
}
答案 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.Create
与Finally
一起打包,以完成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.
}