使用 SqlScaleoutConfiguration 的 SignalR 通知为 Rebus.Transport.TransactionContext 不可序列化提供随机异常

SignalR notification using SqlScaleoutConfiguration gives random exception for Rebus.Transport.TransactionContext not serializable

我使用: SqlScaleoutConfiguration 中的 SignalR 2.2.2 画布 3.0.1

存储在 Rebus 中的一些事件由通知中心处理并使用 signalR 推送到客户端。

一切正常,但今天早上,在发布新版本后,none 的客户收到了 "new version" 消息,可能是因为以下异常:

10:39:04.586| |ERROR| |ProcessId=8196| |ThreadId=5| |SignalR.SqlMessageBus| |Stream 0 : Error starting SQL notification listener: System.Runtime.Serialization.SerializationException: Type 'Rebus.Transport.TransactionContext' in Assembly 'Rebus, Version=3.0.1.0, Culture=neutral, PublicKeyToken=null' is not marked as serializable.

Server stack trace: 
   at System.Runtime.Serialization.FormatterServices.InternalGetSerializableMembers(RuntimeType type)
   at System.Collections.Concurrent.ConcurrentDictionary`2.GetOrAdd(TKey key, Func`2 valueFactory)
   at System.Runtime.Serialization.FormatterServices.GetSerializableMembers(Type type, StreamingContext context)
   at System.Runtime.Serialization.Formatters.Binary.WriteObjectInfo.InitMemberInfo()
   at System.Runtime.Serialization.Formatters.Binary.WriteObjectInfo.InitSerialize(Object obj, ISurrogateSelector surrogateSelector, StreamingContext context, SerObjectInfoInit serObjectInfoInit, IFormatterConverter converter, ObjectWriter objectWriter, SerializationBinder binder)
   at System.Runtime.Serialization.Formatters.Binary.ObjectWriter.Write(WriteObjectInfo objectInfo, NameInfo memberNameInfo, NameInfo typeNameInfo)
   at System.Runtime.Serialization.Formatters.Binary.ObjectWriter.Serialize(Object graph, Header[] inHeaders, __BinaryWriter serWriter, Boolean fCheck)
   at System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Serialize(Stream serializationStream, Object graph, Header[] headers, Boolean fCheck)
   at System.Runtime.Remoting.Channels.CrossAppDomainSerializer.SerializeMessageParts(ArrayList argsToSerialize)
   at System.Runtime.Remoting.Messaging.SmuggledMethodCallMessage..ctor(IMethodCallMessage mcm)
   at System.Runtime.Remoting.Messaging.SmuggledMethodCallMessage.SmuggleIfPossible(IMessage msg)
   at System.Runtime.Remoting.Channels.CrossAppDomainSink.SyncProcessMessage(IMessage reqMsg)

Exception rethrown at [0]: 
   at System.Runtime.Remoting.Proxies.RealProxy.HandleReturnMessage(IMessage reqMsg, IMessage retMsg)
   at System.Runtime.Remoting.Proxies.RealProxy.PrivateInvoke(MessageData& msgData, Int32 type)
   at System._AppDomain.CreateInstance(String assemblyName, String typeName)
   at System.Data.SqlClient.SqlDependency.CreateProcessDispatcher(_AppDomain masterDomain)
   at System.Data.SqlClient.SqlDependency.ObtainProcessDispatcher()
   at System.Data.SqlClient.SqlDependency.Start(String connectionString, String queue, Boolean useDefaults)
   at Microsoft.AspNet.SignalR.SqlServer.ObservableDbOperation.StartSqlDependencyListener()

Rebus 队列中的消息得到正确处理。

处理程序是这样的:

public async Task Handle(ApplicationVersionEvent message)
{
    await
        Clients.All.CheckApplicationVersion(new ApplicationCurrentVersionNotification
        {                    
            CurrentVersion = message.CurrentVersion
        });
}

重启解决了,但我需要了解发生了什么。

我的类似问题是:

但我认为这不是同一种情况。

除了您已经发现的内容之外,我真的很难告诉您这里发生了什么:SignalR 出于某些奇怪的原因 似乎想要序列化隐藏在当前执行上下文,其中一个值是 Rebus 的当前事务上下文。

正如您所包含的链接中所解释的那样,Rebus 在处理消息时以这种方式存储 "ambient transaction",从而允许将其所有操作纳入同一工作单元。

可以使用解释的方法,其中以安全的方式临时删除事务上下文

public async Task Handle(SomeMessage message)
{
    var transactionContext = AmbientTransactionContext.Current;
    AmbientTransactionContext.Current = null;
    try
    {
        JuggleWithAppDomainsInHere();
    }
    finally
    {
        AmbientTransactionContext.Current = transactionContext;
    }
}

可能将相关位分别移动到实现 IDisposable 的 class 中的构造函数/Dispose 方法,使更平滑 API:

using(new DismantleAmbientRebusStuff())
{
    JuggleWithAppDomainsInHere();
}

我认为如果我们要找出真正发生的事情,需要对 SignalR 了解很多的人插话。

我忘记了这个问题,不过后来我通过变通方法解决了它。

线索是 SqlMessageBus 在初始化时序列化上下文,这发生在第一次检索调用 GetHubContext 时,所以我在执行任何命令之前强制初始化它。

    app.MapSignalR();

    var context = new OwinContext(app.Properties);
    var token = context.Get<CancellationToken>("host.OnAppDisposing");
    if (token != CancellationToken.None)
    {
        token.Register(() =>
        {
            log.Info("host.OnAppDisposing");
            // code to run when server shuts down
            BackendMessageBusConfig.DisposeContainers();
        });
    }

    // this code brings forward SignalR SqlMessageBus initialization 
    var forceInit = GlobalHost.ConnectionManager.GetHubContext<NotificationHub>();

    BackendMessageBusConfig.Register();