13

我的 webapp 中存在休眠竞争条件的问题。

我知道在使用旧版本的 log4net 时会发生这种情况(应该在 1.2.10 中修复),尽管我也遇到过这种情况。因此,我们暂时禁用了 log4net,因为竞争条件使 IIS 崩溃,并且在生产中发生这种情况是不可接受的。这发生在加载实体时(参见下面的堆栈跟踪)。除此之外,在 RavenDB 中似乎也出现了类似的问题,请参阅此链接,以及此处链接中没有 NHibernate 的示例。

堆栈跟踪:

Server Error in '/' Application.
Probable I/O race condition detected while copying memory. The I/O package is not thread safe by default. In multithreaded applications, a stream must be accessed in a thread-safe way, such as a thread-safe wrapper returned by TextReader's or TextWriter's Synchronized methods. This also applies to classes like StreamWriter and StreamReader.
Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.

Exception Details: System.IndexOutOfRangeException: Probable I/O race condition detected while copying memory. The I/O package is not thread safe by default. In multithreaded applications, a stream must be accessed in a thread-safe way, such as a thread-safe wrapper returned by TextReader's or TextWriter's Synchronized methods. This also applies to classes like StreamWriter and StreamReader.

Source Error:


Line 105:
Line 106:                if(webUser.Id > 0) { // logged in
Line 107:                    _user = session.Get<User>(webUser.Id);
Line 108:                    if(_user == null) { // session exists, but no user in DB with this id
Line 109:                        new SessionInit().Remove();


Source File: \App_Code\SessionInit.cs    Line: 107

Stack Trace:


[IndexOutOfRangeException: Probable I/O race condition detected while copying memory. The I/O package is not thread safe by default. In multithreaded applications, a stream must be accessed in a thread-safe way, such as a thread-safe wrapper returned by TextReader's or TextWriter's Synchronized methods. This also applies to classes like StreamWriter and StreamReader.]
   System.Buffer.InternalBlockCopy(Array src, Int32 srcOffsetBytes, Array dst, Int32 dstOffsetBytes, Int32 byteCount) +0
   System.IO.StreamWriter.Write(Char[] buffer, Int32 index, Int32 count) +117
   System.IO.TextWriter.WriteLine(String value) +204
   System.IO.SyncTextWriter.WriteLine(String value) +63
   NHibernate.AdoNet.AbstractBatcher.ExecuteReader(IDbCommand cmd) +71
   NHibernate.Loader.Loader.GetResultSet(IDbCommand st, Boolean autoDiscoverTypes, Boolean callable, RowSelection selection, ISessionImplementor session) +580
   NHibernate.Loader.Loader.DoQuery(ISessionImplementor session, QueryParameters queryParameters, Boolean returnProxies) +275
   NHibernate.Loader.Loader.DoQueryAndInitializeNonLazyCollections(ISessionImplementor session, QueryParameters queryParameters, Boolean returnProxies) +205
   NHibernate.Loader.Loader.LoadEntity(ISessionImplementor session, Object id, IType identifierType, Object optionalObject, String optionalEntityName, Object optionalIdentifier, IEntityPersister persister) +590

[GenericADOException: could not load an entity: [app.Presentation.User#338][SQL: SELECT user0_.userID as userID24_0_, user0_.instituteID as institut2_24_0_, user0_.email as email24_0_, user0_.password as password24_0_, user0_.username as username24_0_, user0_.mod_remarks as mod6_24_0_, user0_.lastLogin as lastLogin24_0_, user0_.active as active24_0_, user0_.isAcademic as isAcademic24_0_, user0_.created as created24_0_, (select p.firstName from ej_profile p where p.userID = user0_.userID) as formula11_0_, (select p.lastName from ej_profile p where p.userID = user0_.userID) as formula12_0_, (select p.timeZone from ej_profile p where p.userID = user0_.userID) as formula13_0_ FROM ej_user user0_ WHERE user0_.userID=?]]
   NHibernate.Loader.Loader.LoadEntity(ISessionImplementor session, Object id, IType identifierType, Object optionalObject, String optionalEntityName, Object optionalIdentifier, IEntityPersister persister) +960
   NHibernate.Loader.Entity.AbstractEntityLoader.Load(ISessionImplementor session, Object id, Object optionalObject, Object optionalId) +76
   NHibernate.Loader.Entity.AbstractEntityLoader.Load(Object id, Object optionalObject, ISessionImplementor session) +32
   NHibernate.Event.Default.DefaultLoadEventListener.LoadFromDatasource(LoadEvent event, IEntityPersister persister, EntityKey keyToLoad, LoadType options) +173
   NHibernate.Event.Default.DefaultLoadEventListener.Load(LoadEvent event, IEntityPersister persister, EntityKey keyToLoad, LoadType options) +181
   NHibernate.Event.Default.DefaultLoadEventListener.OnLoad(LoadEvent event, LoadType loadType) +1019
   NHibernate.Impl.SessionImpl.FireLoad(LoadEvent event, LoadType loadType) +403
   NHibernate.Impl.SessionImpl.Get(String entityName, Object id) +469
   NHibernate.Impl.SessionImpl.Get(Type entityClass, Object id) +374
   NHibernate.Impl.SessionImpl.Get(Object id) +391
   SessionInit.GetCurrentUser(ISession session) in j:\dev\app\app_wwwroot\App_Code\SessionInit.cs:107
   DynamicPage.OnPreInit(EventArgs e) in j:\dev\app\app_wwwroot\App_Code\DynamicPage.cs:24
   MemberPage.OnPreInit(EventArgs e) in j:\dev\app\app_wwwroot\App_Code\MemberPage.cs:20
   members_stocks_Default.OnPreInit(EventArgs e) in j:\dev\app\app_wwwroot\members\Default.aspx.cs:28
   System.Web.UI.Page.PerformPreInit() +49
   System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint) +1716

用户的映射:

public class UserViewMapping : ClassMap<User>
{
    public UserViewMapping() {
        Table("ej_user");
        Id(s => s.Id, "userID").GeneratedBy.Native();
        Map(s => s.InstituteId, "instituteID");
        Map(s => s.Email, "email");
        Map(s => s.Password, "password");
        Map(s => s.Name, "username");
        Map(s => s.ModRemarks, "mod_remarks");
        Map(s => s.LastLogin, "lastLogin");
        Map(s => s.Active, "active");
        Map(s => s.IsAcademic, "isAcademic");
        Map(s => s.Created, "created");
        Map(s => s.FirstName).Formula("(select p.firstName from ej_profile p where p.userID = userID)");
        Map(s => s.LastName).Formula("(select p.lastName from ej_profile p where p.userID = userID)");
        Map(s => s.TimeZone).Formula("(select p.timeZone from ej_profile p where p.userID = userID)");
        HasMany<ProfileViewModel>(s => s.Profiles)
            .Table("ej_profile")
            .KeyColumn("userID")
            .Cascade.All()
            .Inverse();
}

一些细节:我使用两个会话来进行查询和命令(以及两个会话工厂),因为我使用了一种类似于 CQRS 的模式。一个用于读取对象的会话,一个用于进行更改的会话(这有助于我保持我的域模型简单并查看模型和映射可能与命令模型不同)。

在我的开发环境(单个用户)中加载用户视图模型时发生竞争条件,但我们确保这在生产中永远不会发生,因为它使 IIS 7 崩溃。此外,在生产中会有多个用户,所以可能是错误可能会更频繁地发生。

此外,我们有很多遗留代码使用 System.Data 和 MySql.Data.MySqlClient.MySqlDataAdapter 来读取/写入数据库。这会不会有影响?

我正在使用 NHibernate 3.1.0(将升级到 3.3.1GA,但这很难重现)和 fluentNhibernate 用于我的映射。

sessionfactories 在 global.asax 中创建:

void Application_Start(object sender, EventArgs e)
{
    QuerySessionFactory.Create(connectionString);
    CommandSessionManager.Initialize(connString);
}

我的页面继承自打开和关闭查询会话的 DynamicPage:

public class DynamicPage : System.Web.UI.Page
{
    protected override void OnPreInit(EventArgs e)
    {
        Session = QuerySessionFactory.Instance.OpenSession();
    }

    protected override void OnUnload(EventArgs e) {
        base.OnUnload(e);
        Session.Close();
    }
}

在 SessionInit 中(从 httpcontext.session 读取 userID,并创建一个“webuser”,一个具有一些简单信息的用户,如 userId)。后来,我把锁放在了周围,并在事务中完成了用户获取请求,不确定它是否有用。

    public IUser GetCurrentUser(ISession session) {
        if(_user == null) { 
            var webUser = new SessionInit().Get;

            if(webUser.Id > 0) { // logged in
                lock(_lock) {
                    using(var tx = session.BeginTransaction()) {
                        _user = session.Get<User>(webUser.Id);
                        tx.Commit();
                    }
                }
                if(_user == null) { // session exists, but no user in DB with this id
                    new SessionInit().Remove();
                }
                ((User)_user)._currentUser = webUser;
            } else {
                if(webUser is CurrentUser && webUser.Id == 0) {
                    if(HttpContext.Current.Session != null) {
                        HttpContext.Current.Response.Cookies.Remove("ASPSESSID");
                        HttpContext.Current.Request.Cookies.Remove("ASPSESSID");
                        HttpContext.Current.Session.RemoveAll();
                        HttpContext.Current.Session.Abandon();
                    }

                    if(HttpContext.Current.Request.Url.Host.Contains("members"))
                        HttpContext.Current.Response.Redirect("/login");
                } else
                    if(webUser.Id == 0) {
                        var userId   = webUser.Id;
                        var userName = webUser.UserName;
                        var loginUrl = webUser.LoginUrl;
                        var clientIp = webUser.ClientIp;
                        var isAdmin  = webUser.IsAdmin();
                        return new eLab.Presentation.Visitor(userId, userName, loginUrl, clientIp, isAdmin, webUser.Theme); 
                    }
            }
            if (_user == null)
                return new eLab.Presentation.Visitor(webUser.Id, webUser.UserName, webUser.LoginUrl, webUser.ClientIp, false, webUser.Theme);
        }
        return _user;
}

命令会话在需要时在 using 块中打开和关闭。

根据堆栈跟踪,问题出现在 StreamWriter -> System.Buffer 中,它再次由 System.IO.SyncTextWriter 调用,它应该是 System.IO.TextWriter 的线程安全包装器。

由于这发生在 TextWriter 中,有没有办法解决这个问题,使用线程安全的 TextWriter?

以我在 DynamicPage 中的方式打开和关闭会话是否安全?

由于这显然难以重现,因此也欢迎任何关于如何做到这一点的想法。

[更新] NHibernate Profiler 告诉我们,我们还在母版页中打开和关闭了一个会话(在 using 块中),因为需要检查当前用户的某些权限,因此每个请求打开两个会话。我已经对其进行了重构,因此它现在不是在页面超类中打开会话,而是在 Application_BeginRequest 上的 global.asax 中打开会话并在 Application_EndRequest 上再次关闭它,其中会话放置在 HttpContext.Current.Items 中。

但是没有确定的测试方法是否可以修复它。

4

2 回答 2

16

Stamppot,感谢您将此问题发布到 StackOverflow;如您所知,在 Web 上没有太多关于此错误消息的其他信息。几个月前,我的团队在一个使用 NHibernate 和 log4net 的 webapp 中遇到了类似的问题。(也可能涉及到 StringTemplate。)我们通过在 Global.ascx.cs 的 Application_Start() 事件处理程序中将 Console.Out/Error 重定向到空流(有效地禁用它们)来“修复”问题:

protected void Application_Start(object sender, EventArgs e)
{
    Console.SetOut(new System.IO.StreamWriter(System.IO.Stream.Null));
    Console.SetError(new System.IO.StreamWriter(System.IO.Stream.Null)); 
}

详细信息:在我们的例子中,“可能的竞争条件......”错误与负载有关。在生产服务器上,此异常会偶尔出现,每次都会使工作进程崩溃。最终我们发现了如何重现它,方法是运行一个脚本,在很短的时间内用许多请求淹没 webapp。当与 NHibernate/StringTemplate/log4net 源代码相关时,异常堆栈跟踪表明使用 Console.Out/Error 方法在各种情况下进行日志记录。出现这种错误似乎是一个奇怪的地方——这些方法不被认为是线程安全的吗?但是,在我们应用上述解决方法后,问题立即消失了,此后再也没有出现过。不幸的是,其他优先事项使我们无法深入挖掘——但无论问题的根本原因是什么,它都没有以任何其他方式表现出来。

于 2013-05-01T06:51:30.240 回答
1

@APW 提供的解决方案的问题是,默认情况下,StreamWriter 不是线程安全的。在这里查看:https ://msdn.microsoft.com/en-us/library/system.io.streamwriter(v=vs.110).aspx

通过将“new StreamWriter”传递给 Console.Set* 您正在传递非线程安全实例。所以我认为再次看到类似的错误只是时间问题。

正确的方法是使用TextWriter.Synchronized方法来包装不安全的 Stream.Null。

using System.IO;
...
var nullStream = TextWriter.Synchronized(TextWriter.Null);
Console.SetOut(nullStream);
Console.SetError(nullStream);

UPD:请忽略这个。我发现 Console.SetOut 将任何流包装到 TextWriter.Synchronized(...) 中。证明。

于 2015-12-23T14:55:11.537 回答