Условие nhibernate гонки при загрузке объекта

У меня проблема с состоянием гонки nhibernate в моем webapp.

Мне это известно при использовании более старых версий log4net (должно быть исправлено в 1.2.10), хотя я также испытал это. Из-за этого мы отключили log4net на данный момент, так как состояние гонки сбой IIS, и это неприемлемо для того, чтобы это произошло на производстве. Это произошло при загрузке объекта (см. Таблицу ниже). Кроме того, аналогичная проблема, похоже, произошла в RavenDB, см. Ссылку и пример без NHibernate здесь link.

StackTrace:

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 or TextWriter 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 or TextWriter 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 or TextWriter 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-подобных шаблонов. Один сеанс для чтения объектов, один для внесения изменений (это помогает мне упростить мою модель домена и просматривать модели и сопоставление, возможно, отличные от командной модели).

Условие гонки возникло при загрузке модели view User в моей среде разработки (один пользователь), но мы уверены, что это никогда не произойдет в производстве, так как оно разбило IIS 7. Также в производстве будет много пользователей, поэтому возможно, ошибка будет происходить чаще.

Кроме того, у нас есть много устаревшего кода, который использует System.Data и MySql.Data.MySqlClient.MySqlDataAdapter для чтения/записи в базу данных. Может ли это повлиять?

Я использую NHibernate 3.1.0 (будет обновлен до 3.3.1GA, но это трудно воспроизвести) и fluentNhibernate для моих сопоставлений.

Сессиифайлы создаются в файле 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 (читает userID из httpcontext.session и создает "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;
}

Командные сеансы открываются и закрываются в используемом блоке при необходимости.

В соответствии с stacktrace проблема возникает в StreamWriter → System.Buffer, которая снова вызывается System.IO.SyncTextWriter, которая должна быть потокобезопасной оболочкой вокруг System.IO.TextWriter.

Так как это произошло в TextWriter, есть ли способ обойти это, чтобы использовать потоковый TextWriter?

Безопасно ли открывать и закрывать сеанс так, как я это делаю в DynamicPage?

Так как это, очевидно, трудно воспроизвести, любые идеи о том, как это сделать, тоже приветствуются.

[ОБНОВЛЕНИЕ] NHibernate Profiler сообщил, что мы также открыли и закрыли сеанс (в блоке использования) на главной странице, так как для проверки некоторых прав для текущего пользователя потребовалось два сеанса, поэтому для каждого запроса было открыто два сеанса. Я отредактировал его, поэтому теперь вместо того, чтобы открывать сеанс в суперклассе страницы, он открывает сеанс в global.asax на Application_BeginRequest и снова закрывает его на Application_EndRequest, где сеанс помещается в HttpContext.Current.Items.

Но нет верного способа тестирования, если это исправляет его.

Ответ 1

Stamppot, спасибо за сообщение этой проблемы в StackOverflow; как вы знаете, в Интернете не так много информации об этом сообщении об ошибке. Моя команда столкнулась с аналогичной проблемой несколько месяцев назад в webapp, которая использует NHibernate и log4net. (Возможно, также был задействован StringTemplate.) Мы "исправили" проблему, перенаправив Console.Out/Error в нулевые потоки (эффективно отключив их) в обработчике событий Application_Start() в Global.ascx.cs:

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

Подробности: В нашем случае ошибка "вероятное состояние гонки..." была связана с нагрузкой. На производственном сервере это исключение будет возникать спорадически, каждый раз разбивая рабочий процесс. В конце концов мы узнали, как воспроизвести его, запустив script, который за короткое время запустил webapp с множеством запросов. Трассировка стека исключений, когда коррелирует с исходным кодом NHibernate/StringTemplate/log4net, указывает на использование методов Console.Out/Error для ведения журнала в различных ситуациях. Кажется, странное место для возникновения такой ошибки --- не эти методы считаются потокобезопасными? Однако после того, как мы применили вышеописанное решение, проблема сразу исчезла и не вернулась с тех пор. К сожалению, другие приоритеты не позволяли нам копать глубже - но, какова бы ни была главная причина проблемы, она не проявлялась ни в каком другом виде.

Ответ 2

Проблема с решением, предоставляемым @APW, заключается в том, что по умолчанию StreamWriter не потокобезопасный. Проверьте это здесь: https://msdn.microsoft.com/en-us/library/system.io.streamwriter(v=vs.110).aspx

Передавая "новый StreamWriter" на Console.Set *, вы передаете небезопасный экземпляр. Поэтому я думаю, что это вопрос времени, чтобы снова увидеть подобную ошибку.

Правильный способ использования метода TextWriter.Synchronized для переноса небезопасного потока .Null.

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

UPD: Пожалуйста, проигнорируйте это. Я обнаружил, что Console.SetOut обертывает любой поток в TextWriter.Synchronized(...). Доказательство.