NHibernate, TransactionScope and locking - nhibernate

I am trying to use TransactionScope with NHibernate in order to call several methods in one transactions. Data repository methods are like this:
public virtual void Save(T dataObject)
{
try
{
using (TransactionScope scope = new TransactionScope(TransactionScopeOption.Required, new TransactionOptions { IsolationLevel = IsolationLevel.RepeatableRead }))
{
this.session.SaveOrUpdate(dataObject);
scope.Complete();
}
}
catch (Exception ex)
{
bool rethrow = ExceptionPolicy.HandleException(ex, "Data Layer Policy");
if (rethrow)
{
throw;
}
}
}
public T GetByNumber(string documentNumber)
{
T document = null;
try
{
using (TransactionScope scope = new TransactionScope(TransactionScopeOption.Required, new TransactionOptions { IsolationLevel = IsolationLevel.RepeatableRead }))
{
document = this.Session.CreateCriteria(typeof(T))
.Add(Restrictions.Eq("Number", documentNumber))
.UniqueResult();
scope.Complete();
}
}
catch (Exception ex)
{
bool rethrow = ExceptionPolicy.HandleException(ex, "Data Layer Policy");
if (rethrow)
{
throw;
}
}
return document;
}
I wanted to test row/table locking in transactions so I made several unit tests and some console applications. Here is code from these console applications:
Application which does update:
const string DocumentNumber = "386774321";
Random randomGenerator = new Random();
using (TransactionScope scope = new TransactionScope(TransactionScopeOption.Required, new TransactionOptions { IsolationLevel = IsolationLevel.RepeatableRead }))
{
using (BillingDocumentRepository billingDocumentRepository = new BillingDocumentRepository())
{
BillingOrderData orderData = billingDocumentRepository.GetByNumber(DocumentNumber);
orderData.Notes = randomGenerator.Next().ToString();
Console.WriteLine(string.Format("SECOND: {0}: Updated notes to {1}.", DateTime.Now.ToString("HH:mm:ss.fffff"), orderData.Notes));
Console.WriteLine(string.Format("SECOND: {0}: Updating order.", DateTime.Now.ToString("HH:mm:ss.fffff")));
Console.WriteLine(string.Format("SECOND: {0}: Going to sleep for 10000ms.", DateTime.Now.ToString("HH:mm:ss.fffff")));
Sleep(10000); // My custom sleep method because I didn't want to use Thread.Sleep for simulating long transaction
billingDocumentRepository.Save(orderData);
}
Console.WriteLine(string.Format("SECOND: {0}: Going to sleep for 10000ms.", DateTime.Now.ToString("HH:mm:ss.fffff")));
Sleep(10000);
Console.WriteLine(string.Format("SECOND: {0}: Completing transaction.", DateTime.Now.ToString("HH:mm:ss.fffff")));
scope.Complete();
}
Application which reads the same row in database:
while (true)
{
using (BillingDocumentRepository repository = new BillingDocumentRepository())
{
Console.WriteLine(string.Format("MAIN: {0}: Getting document.", DateTime.Now.ToString("HH:mm:ss.fffff")));
BillingOrderData billingOrderData = repository.GetByNumber("386774321");
Console.WriteLine(string.Format("MAIN: {0}: Got order with notes {1}.", DateTime.Now.ToString("HH:mm:ss.fffff"), billingOrderData.Notes));
Sleep(1000);
}
}
Problem is that first transaction (which updates row) doesn't lock row for reading at any moment. Second application is reading that row all the time with old value before scope.Complete() and than new value after that. How can I achieve locking with this model?

You should lock when reading. Locking later is "too late":
document = this.Session.CreateCriteria(typeof(T))
.Add(Restrictions.Eq("Number", documentNumber))
.SetLockMode(LockMode.Upgrade)
.SetTimeout(5)
.UniqueResult();
Or:
var doc = session.QueryOver<BillingDocument>()
.Where(c => c.Number== "2233445")
.Lock()
.Upgrade
.UnderlyingCriteria.
SetTimeout(5).
List().
FirstOrNull() as BillingDocument;

There is a session.Lock(object) method.
When you call session.Save(object), NHibernate isn't doing anything in the database until it gets flushed.
Flushing is done (depending on the flush mode, which is usually AutoFlush)
before queries (except Get and Load)
when calling flush explicitly
when committing the transaction (if the connection is created by NH I think)
When the session is flushed, the actual update, insert and delete operations are done on the database and locks are set.
In SQL Server, when the lock is set, the reading transaction is waiting until commit of the updating transaction. When it commits, it reads the committed values (when you are in "Read Committed" isolation).

Related

Sql Dependency - Notification received before processing current request

I am currently using sql dependency notification to detect changes in a table and process them. I am having a problem where the notification gets called while its still in the middle of completing the first request which causes duplicate processing
private void ProcessData()
{
try
{
m_Guids = new List<Guid>();
using (SqlCommand command = new SqlCommand("SP_XXX_SELECT", m_sqlConn))
{
command.CommandType = CommandType.StoredProcedure;
command.Notification = null;
SqlDependency dependency = new SqlDependency(command);
dependency.OnChange += new OnChangeEventHandler(OnDependencyChange);
SqlDependency.Start(m_ConnectionString, m_QueueName);
if (m_sqlConn.State == ConnectionState.Closed)
{
m_sqlConn.Open();
}
using (SqlDataReader reader = command.ExecuteReader())
{
if (reader.HasRows)
{
while (reader.Read())
{
m_Guids.Add(reader.GetGuid(0));
}
}
}
Console.WriteLine(m_Guids.Count.ToString());
ProcessGuids();
}
}
}
catch (Exception ex)
{
//SendFailureEmail
}
}
private void OnDependencyChange(object sender, SqlNotificationEventArgs e)
{
SqlDependency dependency = sender as SqlDependency;
dependency.OnChange -= OnDependencyChange;
ProcessData();
}
public void OnStart()
{
SqlDependency.Stop(m_ConnectionString, m_QueueName);
SqlDependency.Start(m_ConnectionString, m_QueueName);
m_sqlConn = new SqlConnection(m_ConnectionString);
}
ProcessData method gets called again while its still in the middle of processing (processGuids) Should I subscribe to the event after processing all the data?
If I don't subscribe until processing is complete, what happens to the data that was changed during the process, which I believe doesn't get notified until next change happens?. What is the correct way of doing this or am I doing something wrong.
Thanks
SqlDependency.OnChange is called not only on data change.
In the OnDependencyChange you must check e.Type/e.Source/e.Info.
F.e., combination of {Type = Subscribe, Source = Statement, Info = Invalid} means "Statement not ready for notification, no notification started".
See Creating a Query for Notification for SQL statement requirements for notification. You must follow these requirements in SELECT statements in your SP.
Additional requirements for stored procedures are not well documented. Known restrictions for SP:
Use of SET NOCOUNT (ON and OFF) is prohibited.
Use of RETURN is prohibited.

nservicebus and eventstore

I'm wondering if anyone has encountered this before:
I handle a command, and in the handler, I save an event to the eventstore (joliver).
Right after dispatching, the handler for the same command is handled again.
I know its the same command because the guid on the command is the same.
After five tries, nservicebus says the command failed due to the maximum retries.
So obviously the command failed, but I don't get any indication of what failed.
I've put the contents of the dispatcher in a try catch, but there is no error caught. After the code exits the dispatcher, the event handler will always fire as if something errored out.
Tracing through the code, the events are saved to the database (I see the row), the dispatcher runs, and the Dispatched column is set to true, and then the handler handles the command again, the process repeats, and another row gets inserted into the commits table.
Just what could be failing? Am I not setting a success flag somewhere in the event store?
If I decouple the eventstore from nServicebus, both will run as expected with no retries and failures.
The dispatcher:
public void Dispatch(Commit commit)
{
for (var i = 0; i < commit.Events.Count; i++)
{
try
{
var eventMessage = commit.Events[i];
var busMessage = (T)eventMessage.Body;
//bus.Publish(busMessage);
}
catch (Exception ex)
{
throw ex;
}
}
}
The Wireup.Init()
private static IStoreEvents WireupEventStore()
{
return Wireup.Init()
.LogToOutputWindow()
.UsingSqlPersistence("EventStore")
.InitializeStorageEngine()
.UsingBinarySerialization()
//.UsingJsonSerialization()
// .Compress()
//.UsingAsynchronousDispatchScheduler()
// .DispatchTo(new NServiceBusCommitDispatcher<T>())
.UsingSynchronousDispatchScheduler()
.DispatchTo(new DelegateMessageDispatcher(DispatchCommit))
.Build();
}
I had a transaction scope opened on the save that I never closed.
public static void Save(AggregateRoot root)
{
// we can call CreateStream(StreamId) if we know there isn't going to be any data.
// or we can call OpenStream(StreamId, 0, int.MaxValue) to read all commits,
// if no commits exist then it creates a new stream for us.
using (var scope = new TransactionScope())
using (var eventStore = WireupEventStore())
using (var stream = eventStore.OpenStream(root.Id, 0, int.MaxValue))
{
var events = root.GetUncommittedChanges();
foreach (var e in events)
{
stream.Add(new EventMessage { Body = e });
}
var guid = Guid.NewGuid();
stream.CommitChanges(guid);
root.MarkChangesAsCommitted();
scope.Complete(); // <-- missing this
}
}

NHibernate won't let me insert a model in a session if it was part of a failed transaction on that session

Why can't I just insert the model after I get an error back from the database when trying to insert it the first time:
Report report = null;
using (var session = SessionFactory.OpenSession()) {
try {
using (var transaction = session.BeginTransaction()) {
report = new Report();
session.SaveOrUpdate(report);//Exception: Name field required
transaction.Commit();
}
}
catch { }
try {
using (var transaction = session.BeginTransaction()) {
report.Name = "theName";
session.SaveOrUpdate(report);
//Causes Exception:
//Row was updated or deleted by another transaction (or unsaved-value
//mapping was incorrect): [ReportViewer.DataAccess.Models.Report#22]
transaction.Commit();
}
}
catch { }
}
But when I am updating an existing model, and I get an error, I can make my fixes (in this case set a Name) and just try to update again:
Report report = null;
using (var session = SessionFactory.OpenSession()) {
using (var transaction = session.BeginTransaction()) {
report = new Report();
report.Name = "theName";
session.SaveOrUpdate(report);
transaction.Commit();
}
}
using (var session = SessionFactory.OpenSession()) {
//get entity saved from previous session
report = session.Get<Report>(report.Id);
try {
using (var transaction = session.BeginTransaction()) {
report.Name = null;
session.SaveOrUpdate(report);//Exception: Name field required
transaction.Commit();
}
}
catch { }
try {
using (var transaction = session.BeginTransaction()) {
//updates and does not give an error
report.Name = "theName";
session.SaveOrUpdate(report);
transaction.Commit();
}
}
catch { }
}
When an exception triggered by the database occurs, the NHibernate session must be closed (disposed). It is not guaranteed to be consistent (internally or with the DB state) after an exception.
See the chapter on exception handling in the NHibernate reference.
As Oskar said, you should discard an NHibernate session after an exception occurs. However, the reason the insert fails is that you have already made the report persistent by calling SaveOrUpdate on it (you should use Save here). When you call SaveOrUpdate again on the same instance, NHibernate throws an exception because the object is already persistent. Rewriting the code as follows will probably allow the insert to succeed (but it's not recommended):
try {
using (var transaction = session.BeginTransaction()) {
report.Name = "theName";
transaction.Commit();
}
}
In the update example, calling SaveOrUpdate has no effect because the object became persistent when NHibernate loaded it. Understanding NHibernate's instance states and how to work with persistent objects is fundamental and widely misunderstood.
A far better approach is to validate your objects before saving them to the database.

Sql Notification Supported Isolation Levels for Transactions

I am running multiple inserts using transactions. I am using the SqlDependency class to let the client machine know when the server has been updated.
The problem I am having is that whenever I insert using a transaction, no matter what isolation level I set for the transaction, the SqlNotificationEventArgs returns e.Info as Isolation which indicates that I have the wrong isolation level set for that transactions (I think). When I insert without using a transaction, everything runs smoothly.
My questions is, what are the supported Isolation levels, if any, for transactions when using Sql Notification?
Below is some of the code I am using for the notification:
void DataChanged(object sender, SqlNotificationEventArgs e) {
var i = (ISynchronizeInvoke)_form;
if (i.InvokeRequired) {
var tempDelegate = new OnChangeEventHandler(DataChanged);
object[] args = { sender, e };
i.BeginInvoke(tempDelegate, args);
} else {
var dependency = (SqlDependency)sender;
if (e.Type == SqlNotificationType.Change) {
dependency.OnChange -= DataChanged;
GetData(dependency);
}
}
}
And for the transaction:
public void ExecuteNonQueryData(List<string> commandTexts) {
SqlConnection connection = null;
var command = new SqlCommand();
SqlTransaction transaction = null;
try {
connection = new SqlConnection(GetConnectionString());
connection.Open();
transaction = connection.BeginTransaction(IsolationLevel.ReadCommitted);
foreach (var commandText in commandTexts) {
try {
command.Connection = connection;
command.CommandText = commandText;
command.Transaction = transaction;
command.ExecuteNonQuery();
} catch (Exception ex) {
Console.WriteLine(ex.Message);
}
}
transaction.Commit();
} catch (Exception ex) {
Console.WriteLine(ex.Message);
} finally {
command.Dispose();
if (transaction != null) transaction.Dispose();
if (connection != null) {
connection.Close();
connection.Dispose();
}
}
commandTexts.Clear();
}
Edit: I was committing the transaction in the wrong place.
Apparently Query Notification does not support transactions. Removing the transaction code fixed this problem.
According to Microsoft:
Transact-SQL does not provide a way to subscribe to notifications. The CLR data access classes hosted within SQL Server do not support query notifications.
This quote was found at http://msdn.microsoft.com/en-us/library/ms188669.aspx, which describes how Query Notifications work and their requirements.

Nhibernate: Handling an ITransaction Exception So That New Transactions Can Continue with same ISession

I have a list of 10 data objects that I want to insert/update to the database using NHibernate. If one throws an exception (say a primary key violation) I want to still insert/update the other 9. I rolled each object operation into its own atomic transaction, and roll back the transaction if there is an exception. Problem is that if a transaction does cause an exception and is rolled back, on the next transaction Nhibernate complains with the error: null id in Nexus.Data.PortfolioCorporateEntity entry (don't flush the Session after an exception occurs)
My main program is simple. It creates a session from a sessionfactory, creates the data access layer, does some work on the data objects and then tries to persist those data objects to the database.
sessionsManager = new NHibernateSessionManager();
session = sessionsManager.GetSession();
DALC = new NHibernateDataProvider(session);
…
foreach (var pce in pces)
{
try
{
DALC.UpdateOrAddObject<PortfolioCorporateEntity>(pce);
}
catch (Exception ex)
{
Console.WriteLine("Could not add Corporate Entity ID " + pce.CorporateEntity.CorporateEntityID.ToString());
}
}
This is the updateOrAdd procedure in my Nhibernate Data Access Layer, called 10 times for 10 objects.
public void UpdateOrAddObject<T>(T workObject)
{
using (ITransaction tx = mSession.BeginTransaction) {
try {
mSession.SaveOrUpdate(workObject);
mSession.Flush();
tx.Commit();
}
catch (Exception ex) {
tx.Rollback();
throw;
}
}
}
Just to make the point clear, the session is instantiated by the calling program and passed to the Data Access Layer object, constructor of which is below.
public NHibernateDataProvider(ISession session)
{
mSession = session;
}
This works fine except after the exception, it says don’t flush the session after exception. I’m not sure why – transaction was rolled back nicely and the database should be ready to accept another transaction no? What am I doing wrong?
It's not possible to re-use an NHibernate session after an exception is thrown. Quoting the documentation:
If the ISession throws an exception you should immediately rollback the
transaction, call ISession.Close() and discard the ISession instance.
Certain methods of ISession will not leave the session in a consistent state.
So the answer is that you can't do what you're trying to do. You need to create a new session and re-try the updates there.
I clear the session and it continues normally
ISession session = NHibernateHelper.Session;
using (ITransaction transaction = session.BeginTransaction())
{
try
{
session.Update(user, user.UserID);
transaction.Commit();
}
catch (Exception ex)
{
transaction.Rollback();
session.Clear();
throw new DALException("Cannot update user", ex);
}
}
Thanks for the response. Just wanted to make sure it's done right. What you're saying is that my error handling should be simply changed to:
foreach (var pce in pces)
{
try
{
DALC.UpdateOrAddObject<PortfolioCorporateEntity>(pce);
}
catch (Exception ex)
{
Console.WriteLine("Could not add Corporate Entity ID " + pce.CorporateEntity.CorporateEntityID.ToString());
session.Close();
session = sessionsManager.GetSession();
DALC.Session = session;
}
}
Looks like this works just fine. Thanks.