13

Webアプリの競合状態に問題があります。

古いバージョンのlog4net(1.2.10で修正されるはずです)を使用しているときにこれが発生することを認識していますが、これも経験しています。このため、競合状態によってIISがクラッシュし、本番環境でこれが発生することは許容できないため、今のところlog4netを無効にしています。これは、エンティティをロードするときに発生しました(以下のスタックトレースを参照)。これに加えて、同様の問題が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に似たパターンを使用しているため、クエリとコマンドに2つのセッション(および2つのセッションファクトリ)を使用します。オブジェクトを読み取るための1つのセッション、変更を行うための1つのセッション(これにより、ドメインモデルを単純に保ち、モデルとマッピングをコマンドモデルとは異なる可能性があります)表示できます。

開発環境(シングルユーザー)でユーザービューモデルをロードするときに競合状態が発生しましたが、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内(httpcontext.sessionからuserIDを読み取り、userIdなどの単純な情報を持つユーザーである「webuser」を作成します)。後で、私はロックをかけ、トランザクションでユーザーの取得要求を実行しましたが、それが役立つかどうかはわかりません。

    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.TextWriterのスレッドセーフラッパーであると想定されるSystem.IO.SyncTextWriterによって再度呼び出されます。

これはTextWriterで発生したので、これを回避してスレッドセーフなTextWriterを使用する方法はありますか?

DynamicPageで行う方法でセッションを開いたり閉じたりしても安全ですか?

これは明らかに再現が難しいので、それを行う方法についてのアイデアも歓迎します。

[更新]NHibernateProfilerは、現在のユーザーのいくつかの権限を確認する必要があるため、マスターページで(usingブロックで)セッションを開いたり閉じたりしたため、リクエストごとに2つのセッションが開かれたと伝えました。リファクタリングしたので、ページスーパークラスでセッションを開く代わりに、Application_BeginRequestのglobal.asaxでセッションを開き、Application_EndRequestで再び閉じます。セッションはHttpContext.Current.Itemsに配置されます。

しかし、これで修正されるかどうかをテストする確実な方法はありません。

4

2 に答える 2

16

スタンポット、この問題をStackOverflowに投稿していただきありがとうございます。ご存知のように、このエラーメッセージに関する他の情報はWeb上にはあまりありません。私のチームは、数か月前にNHibernateとlog4netを使用するWebアプリで同様の問題に遭遇しました。(StringTemplateも関係している可能性があります。)Global.ascx.csのApplication_Start()イベントハンドラーでConsole.Out / Errorをnullストリームにリダイレクトする(事実上無効にする)ことで、問題を「修正」しました。

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

詳細:この場合、「競合状態の可能性...」エラーは負荷に関連していました。本番サーバーでは、この例外が散発的に発生し、そのたびにワーカープロセスがクラッシュします。最終的に、短期間に多くのリクエストでWebアプリを氾濫させるスクリプトを実行することにより、それを再現する方法を見つけました。例外スタックトレースは、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

「newStreamWriter」を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 に答える