-
Notifications
You must be signed in to change notification settings - Fork 932
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add new session context implementation for TransactionScope #1907
Comments
About 2.,
This means, at "best", flushing the session out of the transaction. At worst, it will fail, the transaction being in a state no more allowing any queries while the session connection would still be enlisted into it. The worst is more likely to occur randomly when the scope was distributed. In such case, the transaction completion event is run on its own thread, concurrently to the second phase of the connection (and of other resources enlisted in the transaction, and even concurrently to the code following the scope disposal), which creates race conditions. (See this comment, and run these tests to see how things can get all mixed and entangled.) Moreover this 2. adds a new case of session being closed from system transactions events, while we are moving away from doing such things. It tends to create connection pool corruption and random failures. If you want more details about those potential troubles, you can find some long read on NH-4011. About 3.,
Better not use hashcode for this. They are not unique keys. Even if unlikely, two different session factories may have the same hashcode. A Update: I forgot, but also better use |
My bad for 2., flushing the session before transaction completion is already done by Nhibernate and doing it again after commit does not make sense. However session closing remains unclear, if I don't close then session when transaction is complete I don't understand when it should be done and by who. #NH-4011 helps to understand the problem but I could not find how session closing is handled. Thanks for pointing the the SessionFactoryImpl uuid. |
This feature is the root cause of the many issues related to NH-4011, and it was even considered to remove it. Instead, it is advised to not rely on it by explicitly flushing the session before scope completion. And then, disable it if possible, by setting the
This all depends on the application. After all, it must have some place to open it, isn't it? Why couldn't it have somewhere to close it? There is also a special case where the session closing is done by NHibernate at the end of the transaction scope: when the user has disposed the session inside the scope, with |
Maybe it is because of my Java and Hibernate experience, but session lifespan == transaction lifespan is a common pattern that makes sense. However keeping the session open for the whole http request can be useful, especially useful for lazy loading in the view (open session in view pattern) but can also lead to n+1 select. In Java this behavior is optional. I don't want my application code to be responsible for opening or closing the session. In many applications calls to service layer are surrounded by a transaction but the service layer is not even aware of the hibernate sessions and should not be responsible for opening it or closing it.
|
The point is, using the transaction completed event (or a resource enlistment) for closing the session cannot be reliable and will create issues in a number of cases. So I do not think adding a session context closing the session from transaction scope completion should be done. It may work only when the scope is not distributed. In such case, the transaction completed events seem to always be executed last, after all second phases of enlisted resources. So the session closing, causing a connection close, should not interfere with the connection second phase. So if your application usage pattern can guarantee its scopes will never go distributed, such a session context could work for you. But better keep it a custom session context of your application. A context provided by NHibernate would have to support broader cases, and it will not be doable reliably. This is not a NHibernate limitation, this is due to how distributed scopes behave. They provide no guarantee that the transaction completed event will run last and not concurrently to second phases of enlisted resources. |
I agree with you for distributed cases, but not every application has distributed transactions so I think it can make sense not only for my use case. 1 session per request, or per thread are two options, 1 per transaction could be another one. Keeping the transaction scope local is a reasonable restriction, but if the connection causes the scope to go distributed it is another problem, not even related with NHibernate. You say that It happens with Odbc, SAP and Sybase, but does that occurs with the most used databases (let's say Sql Server, Postgresql, MySQL and Oracle) ? |
In addition to what has already been written, more than half ot the built-in contextual session implementations are less relevant now : Since a It works well on .NET 5 with postgresql, sql server and mysql ; .NET Core and .NET 5 don't support distributed transactions, which is fine. But postgresql needs class TransactionScopeSessionContext : NHibernate.Context.ICurrentSessionContext
{
private readonly ISessionFactory sessionFactory;
private readonly INHibernateLogger logger;
private readonly Dictionary<string, ISession> activeSessions = new();
public TransactionScopeSessionContext(ISessionFactory sessionFactory)
{
this.sessionFactory = sessionFactory;
this.logger = NHibernateLogger.For(GetType());
}
ISession NHibernate.Context.ICurrentSessionContext.CurrentSession()
{
Transaction? tx = System.Transactions.Transaction.Current;
if (tx == null)
{
throw new InvalidOperationException("cannot manage a session outside a transaction");
}
string txid = tx.TransactionInformation.LocalIdentifier;
logger.Debug("received a call to GetCurrentSession() in transaction {0}", txid);
if (activeSessions.TryGetValue(txid, out ISession? currentSession))
{
logger.Debug("reuse session {0}", currentSession.GetSessionImplementation().SessionId);
return currentSession;
}
else
{
logger.Debug("first call to GetCurrentSession() in this transaction, open a new session");
ISession session = this.sessionFactory.OpenSession();
Guid sessionId = session.GetSessionImplementation().SessionId;
logger.Debug("opened session {0}", sessionId);
logger.Debug("register session {0} for synchronization with transaction {1}", sessionId, txid);
tx.EnlistVolatile(new FlushSessionOnPrepare(session, logger), EnlistmentOptions.EnlistDuringPrepareRequired);
tx.TransactionCompleted += (x, y) =>
{
logger.Debug("close session {0}", sessionId);
session.Close();
logger.Debug("session{0} closed", sessionId);
logger.Debug("remove session {0} from active sessions", sessionId);
activeSessions.Remove(txid);
};
logger.Debug("add session {0} to active sessions under the key {1}", sessionId, txid);
activeSessions.Add(txid, session);
return session;
}
}
}
class FlushSessionOnPrepare : IEnlistmentNotification
{
private readonly ISession session;
private readonly INHibernateLogger logger;
public FlushSessionOnPrepare(ISession session, INHibernateLogger logger) => (this.session, this.logger) = (session, logger);
public void Prepare(PreparingEnlistment preparingEnlistment)
{
if(session.FlushMode == FlushMode.Always || session.FlushMode == FlushMode.Commit || session.FlushMode == FlushMode.Auto)
{
Guid sessionid = session.GetSessionImplementation().SessionId;
logger.Info("flush session {0}", sessionid);
session.Flush();
logger.Info("session {0} flushed", sessionid);
}
preparingEnlistment.Prepared();
}
public void Commit(Enlistment enlistment) => enlistment.Done();
public void InDoubt(Enlistment enlistment) => enlistment.Done();
public void Rollback(Enlistment enlistment) => enlistment.Done();
} |
It should be possible to bind a session lifecycle to a
System.Transactions.TransactionScope
.An implementation of
NHibernate.Context.ICurrentSessionContext
would work as follow (methodGetCurrentSession
).If first call for the current transaction
sessionFactory.OpenSession()
)System.Transactions.Transaction.Current.TransactionCompleted
event, which flush the session (if transaction status isCommitted
) and close it.IDictionary<string, ISession>
where the key is the transaction identifier (Transaction.Current.TransactionInformation.LocalIdentifier
) + sessionfactory hashcode and the value is the session newly created.If not first call for the current transaction : return the session found in the
IDictionary<string, ISession>
I found information about ISession and transaction scope synchronization, but I never got the expected result. I finally wrote an implementation of
ICurrentSessionContext
which works as described above. Is there something wrong in the implementation described here above ?The text was updated successfully, but these errors were encountered: