3

Posting the question and one answer here. Maybe somebody has better answer...

It's possible to write code that triggers a deadlock even for a single user if the developer accidentally opens up a second connection to the database instead of reusing the an existing one (which might already have an open transaction). Certain O/RM and LINQ frameworks make this mistake easy.

Here is the scenario in pure SQL terms:

--Setup:
CREATE TABLE Vendors(VendorID int NOT NULL, VendorName nvarchar(100) NOT NULL, ExtraColumn int not null, CONSTRAINT PK_Vendors PRIMARY KEY CLUSTERED(VendorID))
GO
INSERT INTO Vendors(VendorID,VendorName,ExtraColumn) VALUES (1, 'Microsoft', 12345)
INSERT INTO Vendors(VendorID,VendorName,ExtraColumn) VALUES (2, 'Oracle', 12345)

--Connection 1:
BEGIN TRANSACTION
UPDATE Vendors SET ExtraColumn = 222 WHERE VendorID = 2

--Connection 2:
BEGIN TRANSACTION
SELECT VendorName FROM Vendors WHERE VendorID = 1

This code (and the c#/java/ORM/LINQ/whatever that generated it) might run happily in development/testing when the data is small, but suddenly deadlock when the data/memory profile changes in production and the lock escalates from row to page to table.

So how can I force lock escalation up to table level in my test environment so I can flush out any bugs like this (where the code opens up the 2nd connection)?

Lock escalation is completely controlled by the SQL Server database engine and it's not possible to predict when it will escalate from row lock to table lock. I won't rehash all the details which you can find here, but one in particular to note: "Lock escalation is triggered when...The number of locks in an instance of the Database Engine exceeds memory or configuration thresholds." This means that it could be completely unrelated to my carefully-crafted code and perfectly-selected indices.

explunit
  • 18,967
  • 6
  • 69
  • 94
  • I don't think it's a good idea to always use table lock, which will greatly reduce concurrency. One way to solve the issue is always use row level lock. You can set ALLOW_ROW_LOCKS to ON and ALLOW_PAGE_LOCKS to OFF and enable trace flag 1211 to completely disable lock escalation. – Just a learner Mar 05 '15 at 16:55
  • @OgrishMan this is just for the testing phase to flush out errors in how the code is handling connections. You would never go into production with these settings – explunit Mar 05 '15 at 17:14

1 Answers1

1

One approach is to:

  1. In the test environment, run a script like the following to disable row and page locking on all indexes (making it go directly to table lock)
  2. Run the tests
  3. Run the script again setting it back to normal locking
--
-- Script to disable/enable row & page locking on dev/test environment to flush out deadlock issues
-- executes statement like this for each table in the database:
-- ALTER INDEX indexname ON tablename SET ( ALLOW_ROW_LOCKS  = OFF, ALLOW_PAGE_LOCKS  = OFF )
--
-- DO NOT RUN ON A PRODUCTION DATABASE!!!!
--
set nocount on
declare @newoption varchar(3)
--------------------------------------------------------------------
-- Change variable below to 'ON' or 'OFF' --------------------------
--   'OFF' means row & page locking is disabled and everything 
--      triggers a table lock
--   'ON' means row & page locking is enabled and the server chooses
--     how to escalate the locks (this is the default setting)
set @newoption = 'OFF'
--------------------------------------------------------------------

DECLARE    @TableName varchar(300)
DECLARE    @IndexName varchar(300)
DECLARE @sql varchar(max)

DECLARE inds CURSOR FAST_FORWARD FOR
SELECT tablename, indname
FROM (
    select top 100 percent
    so.name as tablename
         , si.indid
         , si.name as indname
         , INDEXPROPERTY( si.id, si.name, 'IsPageLockDisallowed') as IsPageLockDisallowed
         , INDEXPROPERTY( si.id, si.name, 'IsRowLockDisallowed') as IsRowLockDisallowed
    from   sysindexes si
    join sysobjects so on si.id = so.id
    where  si.status & 64 = 0
      and  objectproperty(so.id, 'IsMSShipped') = 0
      and si.name is not null
      and so.name not like 'aspnet%'
      and so.name not like 'auditLog%'
    order by so.name, si.indid
) t

OPEN inds
FETCH NEXT FROM inds INTO @TableName, @IndexName

WHILE @@FETCH_STATUS = 0
BEGIN

    SET @sql = 'ALTER INDEX [' + @IndexName + '] ON [dbo].[' + @TableName + '] SET ( ALLOW_ROW_LOCKS  = ' + @newoption + ', ALLOW_PAGE_LOCKS  = ' + @newoption +' )'
    PRINT @sql
    EXEC(@sql)

    FETCH NEXT FROM inds INTO @TableName, @IndexName
END

CLOSE inds
DEALLOCATE inds


PRINT 'Done'

Other notes:

  • I got the core of the above script from somebody else's article, but I've long-since forgotten where. Apologies for the lack of attribution.
  • Note that the above script will overwrite any existing custom lock escalation settings on your tables. You can check for these by first running the inner select ("SELECT TOP 100 PERCENT so.name...") to see the existing settings.
explunit
  • 18,967
  • 6
  • 69
  • 94