6

I have been trying to figure out how an error is occurring in my code. The exception tells me a commit is already in progress, but unless a call to SaveChanges is asynchronous, I don't see how that is happening.

I have a Scheduler class that holds multiple Task objects. Each Task has a BackgroundWorker that does processing in another thread. I then have an event handler for this BackgroundWorker complete event in the Task class with the following code:

private void TaskWorkCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    if (!(e.Result is TaskResult))
        throw new ArgumentException("Result must be a TaskResult class.");

    TaskComplete((TaskResult)e.Result);
}

Still with me? So I have this event handler in my task class that fires event TaskComplete that I handle in my main Scheduler class with the following code:

private void TaskCompleted(object sender, TaskCompletedEvent e)
{
    Model.Task scheduledTask = entitySet.Tasks.First(x => x.TaskName == e.ClassName);
    TaskLog logMsg = new TaskLog()
    {
        //stuff here
    };

    scheduledTask.TaskLogs.Add(logMsg);
    entitySet.SaveChanges();
}

Now at this point, to my understanding, I am back in my main thread because the work that is done in my backgroundworker has completed. When I had 5 tasks running very frequently, I was getting an exception on the SaveChanges saying a commit was already in progress. I don't understand how this could be since I am not sharing this context across thread. The only way I could see this happening is if SaveChanges is asynchronous (not a blocking call). I know wrapping the code inside of TaskCompleted with a using statement and a new context would fix it, but I want to know why. And why isn't working in its current state.

One last thing, I am using Telerik's OpenAccess ORM.

Robert Harvey
  • 178,213
  • 47
  • 333
  • 501
Justin
  • 6,373
  • 9
  • 46
  • 72
  • It's not obvious from your code whether you have a UI or not. Is this a server application or something with a user interface? Because if your application does not have a user interface then the BackgroundWorker may behave differently. – RogerN Jan 30 '13 at 14:19
  • [tag:nHibernate] has same kind of limitations – Akim Jan 30 '13 at 14:20
  • @RogerN There is no UI. It runs on a server near continuously. – Justin Jan 30 '13 at 14:20
  • @Akim What kind of limitation specifically? – Justin Jan 30 '13 at 14:21
  • The SynchronizationContext will determine which thread the BackgroundWorker fires its Completed event on. I think in a server environment it's possible to end up with a base implementation of the SynchronizationContext that will fire the Completed event on a random ThreadPool thread. If you want all these completed events to fire on the same thread then you should do your own thread management. – RogerN Jan 30 '13 at 14:27
  • @RogerN Interesting. I thought the BackgroundWorker Completed event was always fired on the thread that created the BackgroundWorker. Knowing that, I don't see how using a BackgroundWorker is very advantageous to use in a lot of cases. – Justin Jan 30 '13 at 14:36
  • 1
    @Justin nhiberante will [perform synchronization with database](http://nhforge.org/doc/nh/en/index.html#transactions-threads) automaticaly, and [calling `commit`, `flush` and other operations from multiple threads is a bug](http://www.hibernatingrhinos.com/products/nhprof/learn/alert/crossthreadsessionusage) – Akim Jan 30 '13 at 14:37
  • 1
    Please fix the title - instead of posting a weak hypothesis as a question, summarize the problem. –  Feb 02 '13 at 20:02
  • I talked to telerik about this issue more in depth and I believe understand what was happening now. – Justin Feb 04 '13 at 15:58

2 Answers2

0

Try to synchronize the commits like this:

private static object _syncObject = new object();

private void TaskCompleted(object sender, TaskCompletedEvent e)
{
    Model.Task scheduledTask = entitySet.Tasks.First(x => x.TaskName == e.ClassName);
    TaskLog logMsg = new TaskLog()
    {
        //stuff here
    };

    scheduledTask.TaskLogs.Add(logMsg);
    lock(_syncObject){
        entitySet.SaveChanges();
    }
}
Adi
  • 5,113
  • 6
  • 46
  • 59
0

You are probably sharing a DbContext between several threads.

If you want to work with the DB in a multithreaded environment it's probably better if you detach the objects (i.e. use no proxies or lazy loading) before start working on the objects. Then simply create a new DbContext for each task, attach the object and then save the changes.

That will however reduce the speed. If you want to share the DbContext context you should probably keep some counter which is increased by each task and then decreased when each task has completed. Finally only save changes when the counter has reached zero again.

jgauffin
  • 99,844
  • 45
  • 235
  • 372