We're updating the issue view to help you get more done. 

Possible race condition in ActionQueue.ExecuteActions


I already put this on the nhibernate-development mailing list because I wasn't sure if this is an actual NHibernate bug (back with 4.0.2GA, but since it apparently still exists over a year later and no replies on the ML, I decided to finally throw this up here (especially because we again tried to upgrade our libraries to latest and 4.1.1GA still shows the issue).

I randomly (and rarely) get an ArgumentOutOfRangeException in ActionQueue.ExecuteActions when committing an ITransaction (created by ISession.BeginTransaction) inside a TransactionScope:

1 2 3 4 5 6 7 8 9 10 11 System.ArgumentOutOfRangeException: Index was out of range. Must be non-negative and less than the size of the collection. Parameter name: index at System.ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument argument, ExceptionResource resource) at System.Collections.Generic.List`1.System.Collections.IList.get_Item(Int32 index) at NHibernate.Engine.ActionQueue.ExecuteActions(IList list) at NHibernate.Engine.ActionQueue.ExecuteActions() at NHibernate.Event.Default.AbstractFlushingEventListener.PerformExecutions(IEventSource session) at NHibernate.Event.Default.DefaultFlushEventListener.OnFlush(FlushEvent event) at NHibernate.Impl.SessionImpl.Flush() at NHibernate.Transaction.AdoTransaction.Commit() at MyNamespace.MyCode.MyClass.DoInTx(Action`1 workInTx) [...]

In particular, the local size is something like 3 or 4 (depending on where I get the Exception and the number of entities affected), while list.Count is 0 when it happens. The loop iteration is mostly at the last index when it does; I've never seen it happen with an index lower than size-1 before.

Code looks pretty much like this:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public void DoInTx(Action<ISession> workInTx) { using (var scope = new TransactionScope(TransactionScopeOption.Required)) { using (var session = SessionFactory.CreateSession()) // calls ISessionFactory.CreateSession { using (var tx = session.BeginTransaction()) { workInTx(session); tx.Commit(); } session.Flush(); } scope.Complete(); } }

(I noticed that committing the transaction also flushes the Session, so the session.Flush() there may not be necessary...)

The given code sample is called from a single thread (there is no manual multithreading going on, nor is there any async/await, Task.Run, Parallel.ForEach or whatever involved), although there are potentially multiple (distinct, non-related) threads running which also use their own instances of ISession. Sessions are not shared between threads, mostly because our own code isn't particularly thread-safe either; but in part also because Session-use is similar to the one illustrated above - using-Blocks inside methods without anything fancy around them.

I wasn't particularly successful in creating a minimum example that reproduces the issue, but my current workaround is patching the method to create a copy first:

1 2 3 4 5 6 7 8 9 10 11 12 13 private void ExecuteActions(IList list) { - int size = list.Count; - for (int i = 0; i < size; i++) - Execute((IExecutable)list[i]); + var entries = new IExecutable[list.Count]; + list.CopyTo(entries, 0); + for (int i = 0; i < entries.Length; i++) + Execute(entries[i]); list.Clear(); session.Batcher.ExecuteBatch(); }

And while this is still potentially problematic, it solved the exceptions we kept seeing every now and then; so it might be related to what ActionQueue.Execute does (either because the passed IExecutable is doing something to the execution list, or because the cleanup actions affect something afterwards)





Frédéric Delaporte


Emanuel Wlaschitz