How to log errors even if the transaction is rolled back? - sql

Lets say we have following commands:
SET XACT_ABORT OFF;
SET IMPLICIT_TRANSACTIONS OFF
DECLARE #index int
SET #index = 4;
DECLARE #errorCount int
SET #errorCount = 0;
BEGIN TRANSACTION
WHILE #index > 0
BEGIN
SAVE TRANSACTION Foo;
BEGIN TRY
-- commands to execute...
INSERT INTO AppDb.dbo.Customers VALUES('Jalal', '1990-03-02');
-- make a problem
IF #index = 3
INSERT INTO AppDb.dbo.Customers VALUES('Jalal', '9999-99-99');
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION Foo; -- I want to keep track of previous logs but not works! :(
INSERT INTO AppDb.dbo.LogScripts VALUES(NULL, 'error', 'Customers', suser_name());
SET #errorCount = #errorCount + 1;
END CATCH
SET #index = #index - 1;
END
IF #errorCount > 0
ROLLBACK TRANSACTION
ELSE
COMMIT TRANSACTION
I want to execute a batch, keep all errors in log and then, if no error was occurred, commit all changes. How can implement it in Sql Server?

The transaction is tied to the connection, and as such, all writes will be rolled back on the outer ROLLBACK TRANSACTION (irrespective of the nested savepoints).
What you can do is log the errors to an in-memory structure, like a Table Variable, and then, after committing / rolling back the outer transaction, you can then insert the logs collected.
I've simplified your Logs and Customers tables for the purpose of brevity:
CREATE TABLE [dbo].[Logs](
[Description] [nvarchar](max) NULL
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO
CREATE TABLE [dbo].[Customers](
[ID] [int] NOT NULL,
[Name] [nvarchar](50) NULL
);
GO
And then you can track the logs in the table variable:
SET XACT_ABORT OFF;
SET IMPLICIT_TRANSACTIONS OFF
GO
DECLARE #index int;
SET #index = 4;
DECLARE #errorCount int
SET #errorCount = 0;
-- In memory storage to accumulate logs, outside of the transaction
DECLARE #TempLogs AS TABLE (Description NVARCHAR(MAX));
BEGIN TRANSACTION
WHILE #index > 0
BEGIN
-- SAVE TRANSACTION Foo; As per commentary below, savepoint is futile here
BEGIN TRY
-- commands to execute...
INSERT INTO Customers VALUES(1, 'Jalal');
-- make a problem
IF #index = 3
INSERT INTO Customers VALUES(NULL, 'Broken');
END TRY
BEGIN CATCH
-- ROLLBACK TRANSACTION Foo; -- Would roll back to the savepoint
INSERT INTO #TempLogs(Description)
VALUES ('Something bad happened on index ' + CAST(#index AS VARCHAR(50)));
SET #errorCount = #errorCount + 1;
END CATCH
SET #index = #index - 1;
END
IF #errorCount > 0
ROLLBACK TRANSACTION
ELSE
COMMIT TRANSACTION
-- Finally, do the actual insertion of logs, outside the boundaries of the transaction.
INSERT INTO dbo.Logs(Description)
SELECT Description FROM #TempLogs;
One thing to note is that this is quite an expensive way to process data (i.e. attempt to insert all data, and then roll back a batch if there were any problems encountered). An alternative here would be to validate all the data (and return and report errors) before attempting to insert any data.
Also, in the example above, the Savepoint serves no real purpose, as even 'successful' Customer inserts will be eventually rolled back if any errors were detected for the batch.
SqlFiddle here - The loop is completed, and despite 3 customers being inserted, the ROLLBACK TRANSACTION removes all successfully inserted customers. However, the log is still written, as the Table Variable is not subjected to the outer transaction.

Related

Service Broker queue "is currently disabled" several times right after upgrading from SQL Server 2012 to 2017, nothing in logs

Yesterday we took our SQL Server 2012 instance which has been processing messages for several years without any issues (except a new periodic performance issue that started several months ago, details below), and upgraded it from SQL Server 2012 to SQL Server 2017+CU29 (KB5010786). We have tried moving compat from 2012 to 2017 and while it helps with a new issues we're seeing, performance is not great.
But the big thing is: since then, we've had the queue spontaneously disable itself twice. It works for minutes/hours, then poof the queue is disabled. We have logging and nothing's giving us anything. We had briefly turned on Query Store, but after the queue disabled the first time, we went looking for similar issue. We found a thread online that said they were having similar problems and that it was Query Store, so we immediately flipped it to Read-Only (we had turned it on in order to try and fix a performance issue we see on Mondays, where it grabs a bad plan and rebooting it seems to be the only fix, but we don't see that the rest of the week). Also of note, this doesn't update rows, it just inserts new rows into a series of hourly tables.
We're also seeing massive locking on LCK_M_IX on where we didn't before. Looking at that next. It's on a part of the code that does an insert into a table, where the output is generated from a CLR. (INSERT INTO table FROM SELECT clr). Moving from 2012 to 2017 seems to have changed that behavior, but it's still seems like it's slow overall, but I'm terrified about it spontaneously disabling again.
We are running the same load on two separate servers, so I have the ability to compare things.
The "disabled" message in our logging table appears several times all at the same time (I'm guessing once per thread). Nothing in the SQL Error Log. Interestingly, in some of the rows in the logging table, the message_body is NULL, but has a body in others. But we see no errors for several minutes before it occurred in either.
The service queue "ODS_TargetQueue" is currently disabled.
We're also running a Extended Event that logs any severity 11+ errors.
All it's showing is
The service queue "ODS_TargetQueue" is currently disabled.
We are also seeing this sporadically which we normally don't see unless we're having log backup issues:
The current transaction cannot be committed and cannot support operations that write to the log file. Roll back the transaction.
We have also seen this a handful of times this morning, which seems to be new:
Process ID 57 attempted to unlock a resource it does not own: METADATA: database_id = 7 CONVERSATION_ENDPOINT_RECV($hash = 0x9904a343:0x9c8327f9:0x4b), lockPartitionId = 0. Retry the transaction, because this error may be caused by a timing condition. If the problem persists, contact the database administrator.
The queue:
CREATE QUEUE [svcBroker].[ODS_TargetQueue] WITH STATUS = ON , RETENTION = OFF , ACTIVATION ( STATUS = ON , PROCEDURE_NAME = [svcBroker].[ODS_TargetQueue_Receive] , MAX_QUEUE_READERS = 30 , EXECUTE AS OWNER ), POISON_MESSAGE_HANDLING (STATUS = ON) ON [PRIMARY]
GO
The procedure
SET QUOTED_IDENTIFIER ON
SET ANSI_NULLS ON
GO
CREATE PROCEDURE [svcBroker].[ODS_TargetQueue_Receive]
AS
BEGIN
set nocount on
-- Variable table for received messages.
DECLARE #receive_table TABLE(
queuing_order BIGINT,
conversation_handle UNIQUEIDENTIFIER,
message_type_name SYSNAME,
message_body xml);
-- Cursor for received message table.
DECLARE message_cursor CURSOR LOCAL FORWARD_ONLY READ_ONLY
FOR SELECT
conversation_handle,
message_type_name,
message_body
FROM #receive_table ORDER BY queuing_order;
DECLARE #conversation_handle UNIQUEIDENTIFIER;
DECLARE #message_type SYSNAME;
DECLARE #message_body xml;
-- Error variables.
DECLARE #error_number INT;
DECLARE #error_message VARCHAR(4000);
DECLARE #error_severity INT;
DECLARE #error_state INT;
DECLARE #error_procedure SYSNAME;
DECLARE #error_line INT;
DECLARE #error_dialog VARCHAR(50);
BEGIN TRY
WHILE (1 = 1)
BEGIN
BEGIN TRANSACTION;
-- Receive all available messages into the table.
-- Wait 5 seconds for messages.
WAITFOR (
RECEIVE TOP (1000)
[queuing_order],
[conversation_handle],
[message_type_name],
convert(xml, [message_body])
FROM svcBroker.ODS_TargetQueue
INTO #receive_table
), TIMEOUT 2000;
IF ##ROWCOUNT = 0
BEGIN
COMMIT;
BREAK;
END
ELSE
BEGIN
OPEN message_cursor;
WHILE (1=1)
BEGIN
FETCH NEXT FROM message_cursor
INTO #conversation_handle,
#message_type,
#message_body;
IF (##FETCH_STATUS != 0) BREAK;
-- Process a message.
-- If an exception occurs, catch and attempt to recover.
BEGIN TRY
IF #message_type = 'svcBroker_ods_claim_request'
BEGIN
exec ParseRequestMessages #message_body
END
ELSE IF #message_type in ('svcBroker_EndOfStream', 'http://schemas.microsoft.com/SQL/ServiceBroker/EndDialog')
BEGIN
-- initiator is signaling end of message stream: end the dialog
END CONVERSATION #conversation_handle;
END
ELSE IF #message_type = 'http://schemas.microsoft.com/SQL/ServiceBroker/Error'
BEGIN
-- If the message_type indicates that the message is an error,
-- raise the error and end the conversation.
WITH XMLNAMESPACES ('http://schemas.microsoft.com/SQL/ServiceBroker/Error' AS ssb)
SELECT
#error_number = CAST(#message_body AS XML).value('(//ssb:Error/ssb:Code)[1]', 'INT'),
#error_message = CAST(#message_body AS XML).value('(//ssb:Error/ssb:Description)[1]', 'VARCHAR(4000)');
SET #error_dialog = CAST(#conversation_handle AS VARCHAR(50));
RAISERROR('Error in dialog %s: %s (%i)', 16, 1, #error_dialog, #error_message, #error_number);
END CONVERSATION #conversation_handle;
END
END TRY
BEGIN CATCH
SET #error_number = ERROR_NUMBER();
SET #error_message = ERROR_MESSAGE();
SET #error_severity = ERROR_SEVERITY();
SET #error_state = ERROR_STATE();
SET #error_procedure = ERROR_PROCEDURE();
SET #error_line = ERROR_LINE();
IF XACT_STATE() = -1
BEGIN
-- The transaction is doomed. Only rollback possible.
-- This could disable the queue if done 5 times consecutively!
ROLLBACK TRANSACTION;
-- Record the error.
BEGIN TRANSACTION;
INSERT INTO svcBroker.target_processing_errors (
error_conversation,[error_number],[error_message],[error_severity],
[error_state],[error_procedure],[error_line],[doomed_transaction],
[message_body])
VALUES (NULL, #error_number, #error_message,#error_severity,
#error_state, #error_procedure, #error_line, 1, #message_body);
COMMIT;
-- For this level of error, it is best to exit the proc
-- and give the queue monitor control.
-- Breaking to the outer catch will accomplish this.
RAISERROR ('Message processing error', 16, 1);
END
ELSE IF XACT_STATE() = 1
BEGIN
-- Record error and continue processing messages.
-- Failing message could also be put aside for later processing here.
-- Otherwise it will be discarded.
INSERT INTO svcBroker.target_processing_errors (
error_conversation,[error_number],[error_message],[error_severity],
[error_state],[error_procedure],[error_line],[doomed_transaction],
[message_body])
VALUES (NULL, #error_number, #error_message,#error_severity,
#error_state, #error_procedure, #error_line, 0, #message_body);
END
END CATCH
END
CLOSE message_cursor;
DELETE #receive_table;
END
COMMIT;
END
END TRY
BEGIN CATCH
-- Process the error and exit the proc to give the queue monitor control
SET #error_number = ERROR_NUMBER();
SET #error_message = ERROR_MESSAGE();
SET #error_severity = ERROR_SEVERITY();
SET #error_state = ERROR_STATE();
SET #error_procedure = ERROR_PROCEDURE();
SET #error_line = ERROR_LINE();
IF XACT_STATE() = -1
BEGIN
-- The transaction is doomed. Only rollback possible.
-- This could disable the queue if done 5 times consecutively!
ROLLBACK TRANSACTION;
-- Record the error.
BEGIN TRANSACTION;
INSERT INTO svcBroker.target_processing_errors (
error_conversation,[error_number],[error_message],[error_severity],
[error_state],[error_procedure],[error_line],[doomed_transaction],
[message_body])
VALUES(NULL, #error_number, #error_message,#error_severity, #error_state, #error_procedure, #error_line, 1, #message_body);
COMMIT;
END
ELSE IF XACT_STATE() = 1
BEGIN
-- Record error and commit transaction.
-- Here you could also save anything else you want before exiting.
INSERT INTO svcBroker.target_processing_errors (
error_conversation,[error_number],[error_message],[error_severity],
[error_state],[error_procedure],[error_line],[doomed_transaction],
[message_body])
VALUES(NULL, #error_number, #error_message, #error_severity, #error_state, #error_procedure, #error_line, 0, #message_body);
COMMIT;
END
END CATCH
END;
GO

Create SQL Server procedure in a transaction

I need to create two procedures in a SQL Server transaction. If failure, I need to rollback the create(s) and any other executed queries in this transaction. I know the create statement must be the first statement in query batch, but I need to know how handle the transaction with multiple batches.
BEGIN TRANSACTION
CREATE PROCEDURE [dbo].[SP_SP-1]
#id BIGINT
AS
BEGIN
SET NOCOUNT ON;
-- SQL statements
END
GO
CREATE PROCEDURE [dbo].[SP_SP-2]
#id BIGINT
AS
BEGIN
SET NOCOUNT ON;
-- SP-2 statements
END
GO
UPDATE Table
SET Value = '1.0.0.5'
COMMIT TRANSACTION / ROLLBACK TRANSACTION
Below is one method to execute multiple batches in a transaction. This uses a temp table to indicate if any batch erred and perform a final COMMIT or ROLLLBACK accordingly.
Another method is to encapsulate statements that must be in single-statement batch (CREATE PROCEDURE, CREATE VIEW, etc.) but that can get rather ugly when quotes within the literal text must be escaped.
CREATE TABLE #errors (error varchar(5));
GO
BEGIN TRANSACTION
GO
CREATE PROCEDURE [dbo].[USP_SP-1]
#id bigint
AS
BEGIN
SET NOCOUNT ON;
-- SP Statments
END;
GO
IF ##ERROR <> 0 INSERT INTO #errors VALUES('error');
GO
CREATE PROCEDURE [dbo].[USP_SP-2]
#id BIGINT
AS
BEGIN
SET NOCOUNT ON;
-- SP-2 Statments
END;
GO
IF ##ERROR <> 0 INSERT INTO #errors VALUES('error');
GO
UPDATE Table SET Value='1.0.0.5'
GO
IF ##ERROR <> 0 INSERT INTO #errors VALUES('error');
GO
IF EXISTS(SELECT 1 FROM #errors)
BEGIN
IF ##TRANCOUNT > 0 ROLLBACK;
END
ELSE
BEGIN
IF ##TRANCOUNT > 0 COMMIT;
END;
GO
IF OBJECT_ID(N'tempdb..#errors', 'U') IS NOT NULL
DROP TABLE #errors;
GO
I suggest you to study more about this subject in Handling Transactions in Nested SQL Server Stored Procedures.
From the beginning, your syntax is wrong. You cannot begin a transaction and then create a procedure, you need to do just the opposite:
CREATE PROCEDURE [dbo].[SP_SP-1]
#id bigint
AS
BEGIN
BEGIN TRY
BEGIN TRANSACTION
SET NOCOUNT ON;
-- SP-2 Statments
Update Table set Value='1.0.0.5'
END TRY
BEGIN CATCH
--handle error and perform rollback
ROLLBACK
SELECT ERROR_NUMBER() AS ErrorNumber
SELECT ERROR_MESSAGE() AS ErrorMessage
END CATCH
END
It is best practice to use TRY and CATCH when attempting to perform update inside transaction scope.
Please read more and investigate using the link I provided to get a bigger picture.
To use Transaction, you need to know what is the meaning of transaction. It's meaning of 'Unit of work either in commit state or rollback state'.
So when you use transaction, you must know that where you declare and where you close. So you must start and end transaction in the parent procedure only than it will work as a unit of work i.e. whatever no of query execute of DML statement, it uses the same transaction.
I do not understand why your update statement outside of procedure and transaction portion too.
It should be (See my comments, you can use TRY Catch same as c sharp) :
Create PROCEDURE [dbo].[SP_SP-1]
#id bigint
AS
BEGIN
Begin Transaction
SET NOCOUNT ON;
-- SP Statments
Exec SP_SP-2 #id --here you can pass the parameter to another procedure, but do not use transaction in another procedure, other wise it will create another transaction
If ##Error > 0 than
Rollback
Else
Commit
End
END
GO
--Do not use transaction in another procedure, otherwise, it will create another transaction which has own rollback and commit and do not participate in the parent transaction
Create PROCEDURE [dbo].[SP_SP-2]
#id BIGINT
AS
BEGIN
SET NOCOUNT ON;
-- SP-2 Statments
END
GO
i find this solution to execute the procedure as string execution , it`s a workaround to execute what i want
Begin Try
Begin Transaction
EXEC ('
Create PROCEDURE [dbo].[SP_1]
#id bigint
AS
BEGIN
SET NOCOUNT ON;
SP-1
END
GO
Create PROCEDURE [dbo].[SP_Inc_Discovery_RunDoc]
#id bigint
AS
BEGIN
SET NOCOUNT ON;
Sp-2
END')
Update Table set Value='1.0.0.5'
Commit
End Try
Begin Catch
Rollback
Declare #Msg nvarchar(max)
Select #Msg=Error_Message();
RaisError('Error Occured: %s', 20, 101,#Msg) With Log;
End Catch

How to get actual exception in SQL transaction when xact_abort is ON

How can I preserve/retrieve the error state or return the actual error when using xact_abort ON?
Currently, when I excecute this stored procedure with an outer transaction already initiated.
begin tran
exec TestFK 2
I get this generic error which hides the actual error
The current transaction cannot be committed and cannot support operations that write to the log file. Roll back the transaction.
But when I execute without an external transaction
exec TestFK 2
I get the proper error.
The INSERT statement conflicted with the FOREIGN KEY constraint "FK__t2__a__3B783965". The conflict occurred in database "XXX", table "dbo.t1", column 'a'.
Setup Code
ALTER procedure [dbo].[TestFK]
#Id int
as
begin
SET NOCOUNT ON
SET xact_abort ON
DECLARE #trancount INT
SET #trancount = ##TRANCOUNT
begin try
IF #trancount = 0
BEGIN TRANSACTION
INSERT INTO t2 VALUES (#Id); -- Foreign key error for #Id = 2
IF #trancount = 0
COMMIT TRANSACTION
end try
begin catch
IF Xact_state() <> 0 AND #trancount = 0
ROLLBACK TRANSACTION
Exec uspInsErrorInfo -- Here I want to preserve the Error State somehow
end catch
END
CREATE TABLE t1 (a INT NOT NULL PRIMARY KEY);
CREATE TABLE t2 (a INT NOT NULL REFERENCES t1(a));
INSERT INTO t1 VALUES (1);
INSERT INTO t1 VALUES (3);
INSERT INTO t1 VALUES (4);
INSERT INTO t1 VALUES (6);
So, the solution I used was to simply remove the try/catch block in this scenario.
Since,
When SET XACT_ABORT is ON, if a Transact-SQL statement raises a
run-time error, the entire transaction is terminated and rolled back.
ALTER procedure [dbo].[TestFK]
#Id int
as
begin
SET NOCOUNT ON
SET xact_abort ON
DECLARE #trancount INT
SET #trancount = ##TRANCOUNT
IF #trancount = 0
BEGIN TRANSACTION
INSERT INTO t2 VALUES (#Id); -- Foreign key error for #Id = 2
-- + some other statements
IF #trancount = 0
COMMIT TRANSACTION
END

MSSQL Prevent rollback when trigger fails

I have an after insert/update/delete trigger, which inserts a new record in an AuditTable every time an insert/update/delete is made to a specific table. If the insertion in the AuditTable fails I'd like the first record to be inserted anyway and the error logged in a further table "AuditErrors".
This is what I have so far and I tried many different things but I can't get this to work if the trigger insert into the AuditTable fails (I test this by misspelling the name of a column in the AuditTable insert). NB: #sql is the insert into the AuditTable.
DECLARE #TranCounter INT
SET #TranCounter = ##TRANCOUNT
IF #TranCounter > 0
SAVE TRANSACTION AuditInsert;
ELSE
BEGIN TRANSACTION;
BEGIN TRY
EXEC (#sql)
IF #TranCounter = 0
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
-- roll back
IF #TranCounter = 0
ROLLBACK TRANSACTION;
ELSE
IF XACT_STATE() <> -1
ROLLBACK TRANSACTION AuditInsert;
-- insert error into database
IF #TranCounter > 0
SAVE TRANSACTION AuditInsert;
ELSE
BEGIN TRANSACTION;
BEGIN TRY
INSERT INTO [dbo].[AuditErrors] ([AuditErrorCode], [AuditErrorMsg]) VALUES (ERROR_NUMBER(), ERROR_MESSAGE())
IF #TranCounter = 0
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
-- roll back
IF #TranCounter = 0
ROLLBACK TRANSACTION;
ELSE
IF XACT_STATE() <> -1
ROLLBACK TRANSACTION AuditInsert;
END CATCH
END CATCH
This is the only way I know of separating the original transaction from the trigger action. In this example the original insert completes even though the audit insert fails. Tested on 2008R2.
It's not pretty but it won't rollback the transaction!
It worked just fine with trusted authentication:
create table TestTable(
ID int identity(1,1) not null
,Info varchar(50) not null
)
GO
create table AuditTable(
AuditID int identity(1,1) not null
,TestTableID int not null
,Info varchar(10) -- The failure is the mismatch in length
)
GO
create procedure insertAudit #id int, #Info varchar(50)
as
set nocount on;
begin try
insert into AuditTable(TestTableID,Info)
values(#id,#Info);
end try
begin catch
select 0
end catch;
GO
create trigger trg_TestTable on TestTable
AFTER INSERT
as
begin
set nocount on;
declare #id int,
#info varchar(50),
#cmd varchar(500),
#rc int;
select #id=ID,#info=Info from inserted;
select #cmd = 'osql -S '+##SERVERNAME+' -E -d '+DB_NAME()+' -Q "exec insertAudit #id='+cast(#id as varchar(20))+',#Info='''+#info+'''"';
begin try
exec #rc=sys.xp_cmdshell #cmd
select #rc;
end try
begin catch
select 0;
end catch;
end
GO
Drop the Audit table and it still completes the original transaction.
Cheers!
Instead of using sqlcmd, you may consider playing with BEGIN TRAN/ROLLBACK a little bit.
Note that, even tho a rollback command will undo every change made since the start of the statement which caused the trigger to fire, any changes made by subsequent commands will not.
All you have to do is to repeat the execution of the code in #sql if the transaction in which data is inserted in the audit table gets rolled back:
TRIGGER BEGINS
<INSERT INSERTED AND DELETED TABLES INTO TABLE VARIABLES, U'LL NEED THEM>
BEGIN TRY
BEGIN TRAN
INSERT INTO AUDITTABLE SELECT * FROM #INSERTED
COMMIT
END TRY
BEGIN CATCH
ROLLBACK
REDO ORIGINAL INSERT/UPDATE/DELETE USING TRIGGER TABLE VARIABLES (#INSERTED AND #DELETED)
INSERT INTO AUDITERROS...
END CATCH
BEGIN TRAN -- THIS IS TO FOOL SQL INTO THINKING THERE'S STILL A TRANSACTION OPEN
TRIGGER ENDS

Commiting only Specific Changes made inside a TRANSACTION which may ROLLBACK

This is a significant edit from the original question, making it more concise and covering the points raised by existing answers...
Is it possible to have mulitple changes made to multiple tables, inside a single transaction, and rollback only some of the changes?
In the TSQL below, I would NOT want any of the changes made by "myLogSP" to ever be rolled back. But all changes made by the various myBusinessSPs should rollback if necessary.
BEGIN TRANSACTION
EXEC myLogSP
EXEC #err = myBusinessSPa
IF (#err <> 0) BEGIN ROLLBACK TRANSACTION RETURN -1 END
EXEC myLogSP
EXEC #err = myBusinessSPb
IF (#err <> 0) BEGIN ROLLBACK TRANSACTION RETURN -1 END
EXEC myLogSP
EXEC #err = myBusinessSPc
IF (#err <> 0) BEGIN ROLLBACK TRANSACTION RETURN -1 END
EXEC myLogSP
COMMIT TRANSACTION
RETURN 0
The order is important, the myLogSPs must happen between and after the myBusinessSPs (the myLogSPs pick up on the changes made by the myBusinessSPs)
It is also important that all the myBusinessSPs happen inside one transaction to maintain database integrity, and allow all their changes to rollback if necessary.
It's as if I want the myLogSPs to behave as if they're not part of the transaction. It is just an inconvenient fact that they happen to be inside one (by virtue of needing to be called between the myBusinessSPs.)
EDIT:
Final answer is "no", the only option is to redesign the code. Either to using table variables for the logging (as variables don't get rolled back) or redesign the business logic to Not require Transactions...
Use SAVEPOINTs, e.g.
BEGIN TRANSACTION
EXEC myLogSP
SAVE TRANSACTION savepointA
EXEC #err = myBusinessSPa
IF (#err <> 0) BEGIN
ROLLBACK TRANSACTION savepointA
COMMIT
RETURN -1
END
EXEC myLogSP
SAVE TRANSACTION savepointB
EXEC #err = myBusinessSPb
IF (#err <> 0) BEGIN
ROLLBACK TRANSACTION savepointB
COMMIT
RETURN -1
END
EXEC myLogSP
SAVE TRANSACTION savepointC
EXEC #err = myBusinessSPc
IF (#err <> 0) BEGIN
ROLLBACK TRANSACTION savepointC
COMMIT
RETURN -1
END
EXEC myLogSP
COMMIT TRANSACTION
EDIT
Based on the information provided so far (and my understanding of it) it appears that you will have to re-engineer you logging SPs, either to use variables, or to use files, or to allow them to run 'after the fact' as follows:
BEGIN TRANSACTION
SAVE TRANSACTION savepointA
EXEC #err = myBusinessSPa
IF (#err <> 0) BEGIN
ROLLBACK TRANSACTION savepointA
EXEC myLogSPA -- the call to myBusinessSPa was attempted/failed
COMMIT
RETURN -1
END
SAVE TRANSACTION savepointB
EXEC #err = myBusinessSPb
IF (#err <> 0) BEGIN
ROLLBACK TRANSACTION savepointB
EXEC myLogSPA -- the call to myBusinessSPa originally succeeded
EXEC myLogSPB -- the call to myBusinessSPb was attempted/failed
COMMIT
RETURN -1
END
SAVE TRANSACTION savepointC
EXEC #err = myBusinessSPc
IF (#err <> 0) BEGIN
ROLLBACK TRANSACTION savepointC
EXEC myLogSPA -- the call to myBusinessSPa originally succeeded
EXEC myLogSPB -- the call to myBusinessSPb originally succeeded
EXEC myLogSPC -- the call to myBusinessSPc was attempted/failed
COMMIT
RETURN -1
END
EXEC myLogSPA -- the call to myBusinessSPa succeeded
EXEC myLogSPB -- the call to myBusinessSPb succeeded
EXEC myLogSPC -- the call to myBusinessSPc succeeded
COMMIT TRANSACTION
You need to basically jump outside of the current context. There are a couple of ways to do that. One (which I have never tried) is to call the CLR to do the insert.
Perhaps a better way though is using the fact that table variables are not affected by transaction. For example:
CREATE TABLE dbo.Test_Transactions
(
my_string VARCHAR(20) NOT NULL
)
GO
DECLARE
#tbl TABLE (my_string VARCHAR(20) NOT NULL)
BEGIN TRANSACTION
INSERT INTO dbo.Test_Transactions (my_string) VALUES ('test point one')
INSERT INTO #tbl (my_string) VALUES ('test point two')
INSERT INTO dbo.Test_Transactions (my_string) VALUES ('test point three')
ROLLBACK TRANSACTION
INSERT INTO dbo.Test_Transactions (my_string) select my_string from #tbl
SELECT * FROM dbo.Test_Transactions
SELECT * FROM #tbl
GO
We have had luck with putting the log entries into table variables and then inserting to the real tables after the commit or rollback.
OK if you aren't on SQL Server 2008, then try this method. It's messy and a workaround but it should work. The #temp table and the table variable would have to be set up with the structure of what is returned by the sp.
create table #templog (fie1d1 int, field2 varchar(10))
declare #templog table (fie1d1 int, field2 varchar(10))
BEGIN TRANSACTION
insert into #templog
Exec my_proc
insert into #templog (fie1d1, field2)
select t.* from #templog t
left join #templog t2 on t.fie1d1 = t2.fie1d1 where t2.fie1d1 is null
insert into templog
values (1, 'test')
rollback tran
select * from #templog
select * from templog
select * from #templog
Use SAVEPOINTS and TRANSACTION ISOLATION LEVELS.
Wouldn't the easy way be to move the log insertion outside the transaction?
I don't really have an answer for you for the table lock, I think you already have the answer, there will have to be a table lock because the identity column may roll back.
move the BEGIN TRANSACTION statement to after the first insert.
Perhaps you could put the inserts/updates to the business tables in their own atomic transaction t1 and wrap each of these transactions in another transaction t2 that executes the log table update and t1 (the business table updates) without any rollbacks. For example:
BEGIN TRANSACTION t2
<insert to log>
<execute stored procedure p1>
END TRANSACTION t2
CREATE PROCEDURE p1
AS
BEGIN TRANSACTION t1
<insert to business tables>
<rollback t1 on error>
END TRANSACTION t1
I believe that when you rollback t1 in the stored procedure this will leave the calling transaction t2 unaffected.