I need to perform distributed transactions in three databases. To that end I inject three session factories in my service. I manage the transaction with the following code:
using(var ts = new TransactionScope(TransactionScopeOption.RequiresNew,
new TransactionOptions() { IsolationLevel = RepeatableRead }))
using(var session1 = _sessionFactory1.OpenSession())
using(var tran1 = session1.BeginTransaction()
using(var session2 = _sessionFactory2.OpenSession())
using(var tran2 = session2.BeginTransaction()
using(var session3 = _sessionFactory3.OpenSession())
using(var tran3 = session3.BeginTransaction()
{
//Entity manipulation
tran1.Commit();
tran2.Commit();
tran3.Commit();
ts.Complete();
}
wich works well and dandy, the problem is that if I issue a return statement before the completion of the transaction, the second session factory gets to an inconsistent state, and in next invocations an exception is thrown when I try to open the session:
Distributed transaction complete. Either enlist this session in a new
transaction or the NULL transaction.
I need the nesting structure because I issue an indetermined number of commands in the entity manipulation part, each of them making changes in the three databases.
A workaround to this problem is avoid the nesting of different sessions, if an object of a previously closed session is needed after, it can be declared out of the using of the origin session and passed to the following, the same can be done with any information neeeded from a previously closed session.
Client client;
using(var session1 = _sessionFactory1.OpenSession())
{
...
client = session1.Get<Client>(myClientId);
...
}
using(var session2 = _sessionFactory2.OpenSession())
{
...
product = _session2.Load<Product>(myProductId);
product.ClientId = client.Id;
...
}
Related
I am investigating some unexpected behavior with NHibernate that needs more clarity.
I create a new object 'Request' and save it. I create another object 'AuditLog' and add request as a reference to AuditLog. I save that too.
Now, if the Request object is evicted from the session (for some reason), and updated again, the references in AuditLog is NULLified in the database when the transaction is committed.
Any ideas on why this would happen?
If the Request object is not created in the session, but retrieved from the database, and the same process runs, the reference in AuditLog is maintained.
Sample code which has been edited for ease in understanding.
If I remove the session.Evict(request1) from the code, the test passes. With this code, when the session closes, an additional query is fired on the DB to null the reference of request in AuditLog.
//Session 1
var session = Resolve<IFullSession>().Session();
using (var tx = session.BeginTransaction())
{
var request1 = new Request { Id = "REQ01" };
request1.SetFieldValue("Type", "Stage1"); //Type is column in Request table
session.Save("Request", request1);
var auditLog1 = new AuditLog { Id = "LOG01" };
auditLog1.SetFieldValue("Request", request1); //Request is reference column to AuditLog
session.Save("AuditLog", auditLog1);
session.Evict(request1);
request1.SetFieldValue("Type", "Stage2");
session.SaveOrUpdate("Request", request1);
tx.Commit();
}
CreateInnerContainers(); // This closes earlier session.
//Session 2
var session2 = Resolve<IFullSession>().Session();
using (var tx = session2.BeginTransaction())
{
var theLogObject = session2.Get<AuditLog>("LOG01");
Assert.IsNotNull(theLogObject); // This is true
Assert.IsNotNull(theLogObject.GetFieldValue("Request")); // This fails
tx.Commit();
}
You can access session objects and use then whatever you like
session.GetSessionImplementation().PersistenceContext.EntityEntries
but if I were you i would make sure that i'm evicting the right object and spend some time on debuging. Knowing what is going on is better than searching for workarounds
foreach (var e in session.GetSessionImplementation().PersistenceContext.EntityEntries.Values.OfType<EntityType>().Where(<condition>))
{
session.Evict(e);
}
The Save call on the session doesn't mean that your entity is written to the underlying database. It will become persisted in the current persistence context (your session).
If you now remove this entity from the persistence context (what you do with "evict"), your session will not be able to save it on flush (transaction end).
Try to call session.Flush() just before session.Evict(request1) and see what happens.
I do not know NHibernate, I'm coming from hibernate, but eventually this helps to clarify.
Within a single transaction scope, I am trying to save data to two separate Raven databases (on the same Raven server).
However, changes in the second session are not being saved to the server.
Here is the code I am using:
var documentStore = new DocumentStore { Url = "http://localhost:8081/" };
documentStore.Initialize();
documentStore.DatabaseCommands.EnsureDatabaseExists("database1");
documentStore.DatabaseCommands.EnsureDatabaseExists("database2");
using (var ts = new TransactionScope(TransactionScopeOption.RequiresNew))
{
using (var session1 = documentStore.OpenSession("database1"))
{
session1.Store(new Entity {Id = "1"});
session1.SaveChanges();
}
using (var session2 = documentStore.OpenSession("database2"))
{
session2.Store(new Entity {Id = "2"});
session2.SaveChanges();
}
ts.Complete();
}
I can see changes in session1 being saved to database1 but changes in session2 are not saved to database2. I've tried with RavenDb build 992 and build 2360
According to the Raven documentation, transactions across databases are supported.
How can I get changes from session2 to be committed?
The distributed transaction needs time to commit. You can either wait briefly with Thread.Sleep before checking your result, or you can set a special property in the session that is checking the result:
session.Advanced.AllowNonAuthoritativeInformation = false;
This seems like a bug, but it is actually by design. Read Working with System.Transactions in the RavenDB documentation.
(FYI - I didn't know this either until I checked.)
The following method queries my database, using a new session. If the query succeeds, it attaches (via "Lock") the result to a "MainSession" that is used to support lazy loading from a databound WinForms grid control.
If the result is already in the MainSession, I get the exception:
NHibernate.NonUniqueObjectException : a different object with the same identifier value was already associated with the session: 1, of entity: BI_OverlordDlsAppCore.OfeDlsMeasurement
when I attempt to re-attach, using the Lock method.
This happens even though I evict the result from the MainSession before I attempt to re-attach it.
I've used the same approach when I update a result, and it works fine.
Can anyone explain why this is happening?
How should I go about debugging this problem?
public static OfeMeasurementBase GetExistingMeasurement(OverlordAppType appType, DateTime startDateTime, short runNumber, short revision)
{
OfeMeasurementBase measurement;
var mainSession = GetMainSession();
using (var session = _sessionFactory.OpenSession())
using (var transaction = session.BeginTransaction())
{
// Get measurement that matches params
measurement =
session.CreateCriteria(typeof(OfeMeasurementBase))
.Add(Expression.Eq("AppType", appType))
.Add(Expression.Eq("StartDateTime", startDateTime))
.Add(Expression.Eq("RunNumber", runNumber))
.Add(Expression.Eq("Revision", revision))
.UniqueResult() as OfeMeasurementBase;
// Need to evict from main session, to prevent potential
// NonUniqueObjectException if it's already in the main session
mainSession.Evict(measurement);
// Can't be attached to two sessions at once
session.Evict(measurement);
// Re-attach to main session
// Still throws NonUniqueObjectException!!!
mainSession.Lock(measurement, LockMode.None);
transaction.Commit();
}
return measurement;
}
I resolved the problem after finding this Ayende post on Cross Session Operations.
The solution was to use ISession.Merge to get the detached measurement updated in the main session:
public static OfeMeasurementBase GetExistingMeasurement(OverlordAppType appType, DateTime startDateTime, short runNumber, short revision)
{
OfeMeasurementBase measurement;
var mainSession = GetMainSession();
using (var session = _sessionFactory.OpenSession())
using (var transaction = session.BeginTransaction())
{
// Get measurement that matches params
measurement =
session.CreateCriteria(typeof(OfeMeasurementBase))
.Add(Expression.Eq("AppType", appType))
.Add(Expression.Eq("StartDateTime", startDateTime))
.Add(Expression.Eq("RunNumber", runNumber))
.Add(Expression.Eq("Revision", revision))
.UniqueResult() as OfeMeasurementBase;
transaction.Commit();
if (measurement == null) return null;
// Merge back into main session, in case it has changed since main session was
// originally loaded
var mergedMeasurement = (OfeMeasurementBase)mainSession.Merge(measurement);
return mergedMeasurement;
}
}
I’m trying to determine the behaviour of multiple database connection in a distributed transaction.
I’ve got a long running process which spawns a series of threads and each thread is then responsible for managing its’ DB connections and such. All of this runs inside of the transaction scope and each thread is enlisted in the transaction via a DependentTransaction object.
When I went to put this process in parallel I ran into a few issues, namely that there appears to be some sort of block preventing the queries from executing at the same time on the transaction.
What I would like to know is how the transaction co-ordinator handles queries from multiple connections to the same DB and if it’s even advisable to pass a connection object across threads?
I’ve read that MS SQL only allows one connection per transaction but I am clearly able to create and initialize more than one connection to the same DB in the same transaction. I’m simply not able to execute the threads in parallel without getting a “Transaction context in use by another session” exception when opening the connections. The result is that the connections have to wait to execute instead of running at the same time and in the end the code runs to completion but there is no net gain to threading the app because of this locking issue.
The code looks something like this.
Sub StartThreads()
Using Scope As New TransactionScope
Dim TL(100) As Tasks.Task
Dim dTx As DependentTransaction
For i As Int32 = 0 To 100
Dim A(1) As Object
dTx = CType(Transaction.Current.DependentClone(DependentCloneOption.BlockCommitUntilComplete), DependentTransaction)
'A(0) = some_other_data
A(1) = dTx 'the Dependent Transaction
TL(i) = Tasks.Task.Factory.StartNew(AddressOf Me.ProcessData, A) 'Start the thread and add it to the array
Next
Tasks.Task.WaitAll(TL) 'Wait for threads to finish
Scope.Complete()
End Using
End Sub
Dim TransLock As New Object
Sub ProcessData(ByVal A As Object)
Dim DTX As DependentTransaction = A(1)
Dim Trans As Transactions.TransactionScope
Dim I As Int32
Do While True
Try
SyncLock (TransLock)
Trans = New Transactions.TransactionScope(DTX, TimeSpan.FromMinutes(1))
End SyncLock
Exit Do
Catch ex As TransactionAbortedException
If ex.ToString.Contains("Failure while attempting to promote transaction") Then
ElseIf ex.Message = "The transaction has aborted." Then
Throw New Exception(ex.ToString)
Exit Sub
End If
I += 1
If I > 5 Then
Throw New Exception(ex.ToString)
End If
Catch ex As Exception
End Try
Thread.Sleep(10)
Loop
Using Trans
Using DALS As New DAC.DALScope
Do While True
Try
SyncLock (TransLock)
'This opens two connection to the same DB for later use.
DALS.CurrentDAL.OpenConnection(DAC.DAL.ConnectionList.FirstConnection)
DALS.CurrentDAL.OpenConnection(DAC.DAL.ConnectionList.SecondConnection)
End SyncLock
Exit Do
Catch ex As Exception
'This is usually where I find the bottleneck
'"Transaction context in use by another session" is the exception that I get
Thread.Sleep(100)
End Try
Loop
'*****************
'Do some work here
'*****************
Trans.Complete()
End Using
End Using
DTX.Complete()
End Sub
EDIT
My tests have conclusively showed that this just can't be done. Even if there is more than one connection or the same connection is used all request s in the transaction or the questions are processed sequentially.
Perhaps they will change this behaviour in the future.
First, you have to separte what you read here and there about SQL Server transactions into 2 distinct cases: local and distributed.
Local SQL transactions:
SQL Server allows only one request to execute on each local transaction.
By default only one session can enroll in a local transaction. Using sp_getbindtoken and sp_bindsession multiple sessions can be enrolled in a local transaction. The sessions are still restricted to only one executing a request at any time.
With Multiple Active Result Sets (MARS) one sessions can execute multiple requests. All requests have to be enrolled in the same local transaction.
Distributed Transactions:
Multiple sessions can have their local transaction enrolled in a single distributed transaction.
Each session is still enroled in a local transaction, subject to all restrictions mentioned above for local transactions
Local transactions enroled in a distributed transaction are subject to two phase commit coordinated by the distributed transaction
All local transactions on an instance enrolled in a distributed transaction are still independent local transactions, primarily meaning they have conflicting lock namespaces.
So when a client creates a .Net TransactionScope and under this transaction scope it executes multiple requests on the same server, these requests are all local transactions enrolled in a distributed transaction. A simple example:
class Program
{
static string sqlBatch = #"
set nocount on;
declare #i int;
set #i = 0;
while #i < 100000
begin
insert into test (a) values (replicate('a',100));
set #i = #i+1;
end";
static void Main(string[] args)
{
try
{
TransactionOptions to = new TransactionOptions();
to.IsolationLevel = IsolationLevel.ReadCommitted;
using (TransactionScope scp = new TransactionScope(TransactionScopeOption.Required, to))
{
using (SqlConnection connA = new SqlConnection(Settings.Default.connString))
{
connA.Open();
using (SqlConnection connB = new SqlConnection(Settings.Default.connString))
{
connB.Open();
SqlCommand cmdA = new SqlCommand(sqlBatch, connA);
SqlCommand cmdB = new SqlCommand(sqlBatch, connB);
IAsyncResult arA = cmdA.BeginExecuteNonQuery();
IAsyncResult arB = cmdB.BeginExecuteNonQuery();
WaitHandle.WaitAll(new WaitHandle[] { arA.AsyncWaitHandle, arB.AsyncWaitHandle });
cmdA.EndExecuteNonQuery(arA);
cmdB.EndExecuteNonQuery(arB);
}
}
scp.Complete();
}
}
catch (Exception e)
{
Console.Error.Write(e);
}
}
}
Create a dummy test table:
create table test (id int not null identity(1,1) primary key, a varchar(100));
and run the code in my sample. You will see that both requests are executing in parallel, each one isnerting 100k rows in the table, then both commit when the transaction scope is complete. So the problems you're seeing are no related to SQL Server nor to TransactionScope, they can easily handle the scenario you describe. More, the code is very simple and straight forward and there isn't any need for dependent transactions to be created, cloning to occur nor transactions to be promotted.
Updated
Using explicit threads and dependent transactions:
private class ThreadState
{
public DependentTransaction Transaction {get; set;}
public EventWaitHandle Done {get; set;}
public SqlConnection Connection { get; set; }
}
static void Main(string[] args)
{
try
{
TransactionOptions to = new TransactionOptions();
to.IsolationLevel = IsolationLevel.ReadCommitted;
using (TransactionScope scp = new TransactionScope(TransactionScopeOption.Required, to))
{
ThreadState stateA = new ThreadState
{
Transaction = Transaction.Current.DependentClone(DependentCloneOption.BlockCommitUntilComplete),
Done = new AutoResetEvent(false),
Connection = new SqlConnection(Settings.Default.connString),
};
stateA.Connection.Open();
ThreadState stateB = new ThreadState
{
Transaction = Transaction.Current.DependentClone(DependentCloneOption.BlockCommitUntilComplete),
Done = new AutoResetEvent(false),
Connection = new SqlConnection(Settings.Default.connString),
};
stateB.Connection.Open();
ThreadPool.QueueUserWorkItem(new WaitCallback(Worker), stateA);
ThreadPool.QueueUserWorkItem(new WaitCallback(Worker), stateB);
WaitHandle.WaitAll(new WaitHandle[] { stateA.Done, stateB.Done });
scp.Complete();
//TODO: dispose the open connections
}
}
catch (Exception e)
{
Console.Error.Write(e);
}
}
private static void Worker(object args)
{
Debug.Assert(args is ThreadState);
ThreadState state = (ThreadState) args;
try
{
using (TransactionScope scp = new TransactionScope(state.Transaction))
{
SqlCommand cmd = new SqlCommand(sqlBatch, state.Connection);
cmd.ExecuteNonQuery();
scp.Complete();
}
state.Transaction.Complete();
}
catch (Exception e)
{
Console.Error.WriteLine(e);
state.Transaction.Rollback();
}
finally
{
state.Done.Set();
}
}
Considering the following code blocks, why does call to HQL work but call to delete() not work? As a background, I'm using NHibernate over IBM.Data.DB2.Iseries driver. Come to find out, journaling on the AS400 is turned off so I can't use transactions. I'm not the AS400 admin or know anything about it so I don't know if having journaling turned off (not opening transactions) is causing this problem or not. Do I absolutely need the ability to open transactions if I'm calling Delete() or other NHibernate functions?
//This Does not work - no error and no deletes
public static void Delete(Object Entity)
{
using (ISession session = _sessionFactory.OpenSession())
{
//using(ITransaction tx = session.BeginTransaction())
//{
session.Delete(Entity);
//tx.Commit();
session.Close();
//}
}
}
//This does work
public static void Delete(Object Entity)
{
using (ISession session = _sessionFactory.OpenSession())
{
//commented out transaction control because throws error because
//journaling is not turned on the AS400
//using(ITransaction tx = session.BeginTransaction())
//{
session.CreateQuery("delete MyDAO p where p.MyDAOID = :MyDAOID").SetString("MyDAOID", ((MyDAO)Entity).MyDAOID.ToString()).ExecuteUpdate();
//}
}
}
Try calling session.Flush() after you delete, but before you close the session. And you don't need to explicitly close the session:
using (var session = ...)
{
entity.Delete();
session.Flush();
}
After delving further into this, I found that this works but why?:
public static void Delete(Object Entity)
{
using (ISession session = _sessionFactory.OpenSession())
{
MyDAO p = session.Get<MyDAO>(Entity.ID);
session.Delete(p);
session.Flush();
}
}
Here is my scenario: I have previously queried for a list of objects which I populated into a Grid. For that query process, I opened/closed a session. Upon calling my delete function, I take that object and pass it into this function for a separate open/close session process. To stir things up a bit, I added the call to this new delete asking it to get the object first (the one I want deleted), then call delete. Now it sends the delete query to the database and the object is in fact deleted. Why is this?
Is this because my original query of objects was opened within a different session and then deleted in another? Does this have anything to do with the unit of work. Should I open my session for my grid, leave it open until my form closes so all deletes work inside that session? I was under the impression that sessions should be opened/closed quickly. I'm confused.