Search

Some transactions become doomed apparently not as documented by Ionuț-Gabriel Nica

Closed
as By Design Help for as By Design

1
1
Sign in
to vote
Type: Bug
ID: 485508
Opened: 8/27/2009 2:06:25 AM
Access Restriction: Public
0
Workaround(s)
0
User(s) can reproduce this bug
Hello,

Let's assume executed the following two prerequisite statements on an existing database:

CREATE TABLE Temp
(
    Field int not null
);

CREATE TRIGGER TG_Temp
    ON Temp
    INSTEAD OF INSERT
AS
BEGIN

    RAISERROR(N'Insertions are not allowed.', 16, 1);

END

Now, please consider the following statements, which consist the objective of the questions and the model for some real important situations:

BEGIN TRAN

-- creating a savepoint:
SAVE TRAN sp;

BEGIN TRY

    -- because of the intended trigger, the following statement will raise an expected error ("Insertions are not allowed.") with a severity of 16, so the control will be passed to the CATCH block:
    INSERT INTO Temp (Field)
    VALUES (NULL);

END TRY
BEGIN CATCH

    PRINT error_message();
    -- at this point the transaction has (strangely?) become doomed (uncommittable), so the following desired statement will raise a typically error ("The current transaction cannot be committed and cannot be rolled back to a savepoint. Roll back the entire transaction.") with a severity of 16, so the execution will terminate:
    ROLLBACK TRAN sp;

END CATCH

-- this statement will never be hit:
COMMIT;

(#1) The primary question is why the transaction becomes doomed, because according to the official documentation (see the links bellow):

"A transaction enters an uncommittable state inside a TRY block when an error occurs that would otherwise have ended the transaction."

For SQL Server 2008: http://msdn.microsoft.com/en-us/library/ms179296.aspx
For SQL Server 2005: http://msdn.microsoft.com/en-us/library/ms179296%28SQL.90%29.aspx

The following simple code demonstrates that the used insert statement does not end a transaction:

BEGIN TRAN;

INSERT INTO Temp (Field)
VALUES (NULL);

-- because the transaction has not ended, 1 will be printed:
PRINT @@trancount;

ROLLBACK;

Now, if we disable the trigger (ALTER TABLE Temp DISABLE TRIGGER TG_Temp;) and run again the statements which consist the objective of the questions, everything goes just as expected: an error occurs ("Cannot insert the value NULL into column 'Field', table 'Temp'; column does not allow nulls. INSERT fails."), the control is passed to the CATCH block, but the transaction is in a normal committable state, so the rollback to savepoint statement does not fail and the final commit statement is hit.

This unexpected (or apparently non-according to the documentation) behavior has major implications, because makes in some way TRY...CATCH blocks unuseful in certain situations, as long as they can doom transactions in some strange ways.

Finally, the secondary and the last question is related to the things discussed above. Let's run separately the following statements (the trigger remains disabled):

-- this will cause an error with a severity of 16:
INSERT INTO Temp (Field)
VALUES (NULL);
-- this statement will be hit:
PRINT 'x';

And now, run the following statements:

DECLARE @var int;
-- this will also cause an error with a severity of 16:
SET @var = convert(int, 'x');
-- this statement will never be hit:
PRINT 'x';

(#2) The question is why in the first situation the execution continues and opened transactions (if exist) remain intact (this is the expected behavior) and in the second situation the execution stops and opened transactions are rolled back, taking in consideration that in both cases the error's severity is 16 (determined by using TRY...CATCH blocks end error_severity() function)? Does exist any other factors (like implicit transactions for example) other than error's severity that influence the behavior or is there some official documentation that can help?

Thank you very much!

Sincerely,
Ionuț-Gabriel Nica
Software Developer
Details (expand)
Product Language
English

Version

SQL Server 2005 SP3

Category

SQL Engine

Operating System

Win2003 Enterprise Server (SP2)
Operating System Language
English
Steps to Reproduce
Execute the following SQL script:

CREATE TABLE Temp
(
Field int not null
);

CREATE TRIGGER TG_Temp
ON Temp
INSTEAD OF INSERT
AS
BEGIN

RAISERROR(N'Insertions are not allowed.', 16, 1);

END

BEGIN TRAN

SAVE TRAN sp;

BEGIN TRY

INSERT INTO Temp (Field)
VALUES (NULL);

END TRY
BEGIN CATCH

ROLLBACK TRAN sp;

END CATCH

COMMIT;
Actual Results
Error: The current transaction cannot be committed and cannot be rolled back to a savepoint. Roll back the entire transaction.
Expected Results
Normal exit (transaction not to become doomed, so rollback to savepoint and commit to succeed).

Platform

32
File Attachments
0 attachments
Sign in to post a comment.
Posted by Ionuț-Gabriel Nica on 6/3/2011 at 7:19 AM
Hi Anoop,

Thanks for your example, it is very good and this time it has not to do with triggers and/or the XACT_ABORT setting, at least directly. Your code can also be simplified more, to the following:

BEGIN TRAN;

SAVE TRAN sp;

BEGIN TRY
    
    EXEC sp_executesql N'DROP TABLE xyz';
    
END TRY
BEGIN CATCH
    
    -- this statement will cause an error because the transaction becames doomed:
    ROLLBACK TRAN sp;
    
END CATCH

-- this statement will never be hit:
COMMIT;

After this, you can separately execute the following code:

BEGIN TRAN

EXEC sp_executesql N'DROP TABLE xyz';

-- because the transaction has not ended, 1 will be printed:
PRINT @@trancount;

ROLLBACK;

Again, the statement is proved to not end a transaction, so this fact contradicts the documentation regarding entering in a uncommitable state.

Thanks!

Ionut
Posted by Anoop Sihag on 2/23/2011 at 9:19 AM
Just forget to mention that if you don't have xyz table then it will jump to catch part and you will get the doomed error.
Posted by Anoop Sihag on 2/23/2011 at 9:15 AM
I don't find the solution here.


Let us take a new example wchich is more similar to Ionut-Gabriel Nica case.

I have create a procedure as below
CREATE PROCEDURE spDoomedTransaction
AS

SET NOCOUNT ON

BEGIN TRY

    DECLARE @IsOuterTrans BIT = CASE WHEN @@TRANCOUNT > 0 THEN 1 ELSE 0 END
        DECLARE @RollbackPoint NCHAR(32) = REPLACE(CONVERT(NCHAR(36), NEWID()), N'-', N'')

    IF @IsOuterTrans = 1        
     SAVE TRANSACTION @RollbackPoint
    ELSE
     BEGIN TRANSACTION @RollbackPoint
    
        
        
        
    EXECUTE sys.sp_ExecuteSql N'DROP TABLE xyz'
    

    IF @IsOuterTrans = 0
     COMMIT TRANSACTION @RollbackPoint;

RETURN 0
END TRY
BEGIN CATCH

    /* Rollback the current transaction */
    IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION @RollbackPoint
END CATCH

GO
----------------------

--call the sp spDoomedTransaction inside transaction

BEGIN TRAN
EXECUTE spDoomedTransaction
ROLLBACK
GO
-----------
Now, you have doomed transaction error as mentioned below

The current transaction cannot be committed and cannot be rolled back to a savepoint. Roll back the entire transaction.
Posted by Microsoft on 9/9/2009 at 3:27 PM
Dear Ionut-Gabriel Nica,

Please let me answer your second question about why different errors with same severity result in different error handling behavior. The answer is, the error handling is case-by-case. It depends on not only the serverity, but also the error type and context. Unfortunately, there is no published list of error handling behavior for different errors. In general, only servere errors should kill the connection and extremely ones shutdown server. But when it comes to statement abort vs transaction abort, it is hard to summarize the rules -- i.e. it is case-by-case.

I hope this helps. Tobias provided soluation to your first question. Please let us know if we can help you with anything else.

Thanks,
Leo Huang
Posted by Microsoft on 9/2/2009 at 10:53 PM
Hi Ionut-Gabriel,

You have run into an interesting corner case in T-SQL. Code inside a trigger behaves slightly different when it comes to errors and transactions compared to code execute from outside a trigger. Basically the setting SET XACT_ABORT is by default set to OFF by default but to ON inside triggers. Because of this errors thrown inside a trigger that gets caught by a catch block OUTSIDE of the trigger are uncommittable. The work around is simply to set XACT_ABORT to OFF in your trigger before you raise the error.

I hope this helps you!
- Tobias, SQL Server Engine
Sign in to post a workaround.