I have the following deadlock graph that describes two sql statements that are mutually locked to each other. I'm just not sure how to analyze this problem, and then fix my sql code so that this does not happen.
Main Deadlock Schedule
alt text http://img140.imageshack.us/img140/6193/deadlock1.png Click here to enlarge the image .
Left side details
alt text http://img715.imageshack.us/img715/3999/deadlock2.png Click here to enlarge the image.
Right side details
alt text http://img686.imageshack.us/img686/5097/deadlock3.png Click here to enlarge the image.
XML file with six stub files
Click here to download the xml file .
Table layout
alt text http://img509.imageshack.us/img509/5843/deadlockschema.png
LogEntries Table Elements
alt text http://img28.imageshack.us/img28/9732/deadlocklogentriestable.png
Connected Clients Information
alt text http://img11.imageshack.us/img11/7681/deadlockconnectedclient.png
What does the code do?
I am reading in several files (for example, say 3, for this example) at the same time. Each file contains different data, but the same data type. Then I insert the data into the LogEntries table, and then (if necessary) I insert or delete something from the ConnectedClients table.
Here is my sql code.
using (TransactionScope transactionScope = new TransactionScope()) { _logEntryRepository.InsertOrUpdate(logEntry); // Now, if this log entry was a NewConnection or an LostConnection, then we need to make sure we update the ConnectedClients. if (logEntry.EventType == EventType.NewConnection) { _connectedClientRepository.Insert(new ConnectedClient { LogEntryId = logEntry.LogEntryId }); } // A (PB) BanKick does _NOT_ register a lost connection .. so we need to make sure we handle those scenario as a LostConnection. if (logEntry.EventType == EventType.LostConnection || logEntry.EventType == EventType.BanKick) { _connectedClientRepository.Delete(logEntry.ClientName, logEntry.ClientIpAndPort); } _unitOfWork.Commit(); transactionScope.Complete(); }
Now each file has its own instance of UnitOfWork (which means that it has its own database connection, transaction context and repository). Therefore, I assume that this means that at the same time, all three connections to db occur simultaneously.
Finally, it uses the Entity Framework as a repository, but please don't let it stop thinking about this issue.
Using a profiling tool, Isolation Level is Serializable . I also tried ReadCommited and ReadUncommited , but they both mistakenly: -
ReadCommited : same as above. Dead end.ReadUncommited : different error. EF exception, which states that he expected some result, but received nothing. I assume this is a LogEntryId Identity ( scope_identity ) scope_identity , which is expected but will not be received due to a dirty read.
Please, help!
PS. This is Sql Server 2008, by the way.
Update # 2
After reading Remus Rusanu's updated answer, I felt like I could try and provide a little more information to see if anyone else could help.
EF chart
alt text http://img691.imageshack.us/img691/600/deadlockefmodel.png
Now, Remus is offering (and mind you, he says he is not familiar with EF) ...
The last piece of the puzzle, the inexplicable lock on the left, the node has on PK_ConnectedClients, I will assume that from the implementation of EF InsertOrUpdate. It is likely to search first, but because of FK, the relationship declared between ConnectedClients and LogEntries is searched on PK_ConnectedClients, therefore acquiring a serializable lock.
Interesting. I'm not sure why the left node has a lock on PK_ConnectedClients , as suggested above. Ok, let's check the code for this method ....
public void InsertOrUpdate(LogEntry logEntry) { LoggingService.Debug("About to InsertOrUpdate a logEntry"); logEntry.ThrowIfArgumentIsNull("logEntry"); if (logEntry.LogEntryId <= 0) { LoggingService.Debug("Current logEntry instance doesn't have an Id. Instance object will be 'AddObject'."); Context.LogEntries.AddObject(logEntry); } else { LoggingService.Debug("Current logEntry instance has an Id. Instance object will be 'Attached'."); Context.LogEntries.Attach(logEntry); } }
Hm. this is a simple AddObject (aka. Insert) or Attach (aka. Update). No links. The Sql code also does not indicate any material to search.
Ok, then ... I have two more methods ... maybe they do some searching?
In ConnectedClientRepository ...
public void Insert(ConnectedClient connectedClient) { connectedClient.ThrowIfArgumentIsNull("connectedClient"); Context.ConnectedClients.AddObject(connectedClient); }
Nope β also basic, as.
Last Lucky Way? Wow .. now this is interesting ....
public void Delete(string clientName, string clientIpAndPort) { clientName.ThrowIfArgumentIsNullOrEmpty("clientName"); clientIpAndPort.ThrowIfArgumentIsNullOrEmpty("clientIpAndPort");
So, looking above, I take an instance of the record I want to delete .. and if it exists, then delete it.
So, if I comment on this method call, in my original logical path to the top of this SO-mail ... what happens?
works. WOWZ.
It also works as Serializable or Read Commited - both work if I don't call the Delete method.
So why will this delete method get a lock? . Is there a blockage and some kind of deadlock due to the choice (with Serializable )?
With read committed , is it possible that I have 3 delete calls performed simultaneously.
- 1st captures a data instance.
- 2nd (and 3rd) captures another instance of the same data.
- Now, 1st removal. well.
- 2nd deletes .. but the line is gone .. so I get this strange error about the affected number of lines (0). <== null items removed.
Possible? If so .. uh ... how can I fix this? Is this a classic race condition case? Is there any way to prevent this?
Update
- Fixed links to images.
- Link to the source XML deadlock file. Here is the same link .
- Added database table schema.
- Both tables added.