加载实体时的nhibernate竞争条件

时间:2012-09-28 11:05:54

标签: .net nhibernate race-condition

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

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

堆栈跟踪:

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;
}

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

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

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

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

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

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

但是没有确定的方法可以解决这个问题。

2 个答案:

答案 0 :(得分:13)

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方法记录各种情况。看起来像是出现这样一个错误的奇怪地方---不是这些方法considered to be thread-safe?但是,在我们应用上述解决方法之后,问题立即消失并且从那时起就没有返回。不幸的是,其他优先事项使我们无法深入挖掘---但无论问题的根本原因是什么,它都没有以任何其他方式表现出来。

答案 1 :(得分:0)

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

传递&#34;新的StreamWriter&#34;到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(...)中。 Proof.