I encountered a problem using T-SQL / SQL server (2017/140):
In my scenario there are a number of stored procedures (= worker procedures), which in turn perform different tasks and at the end output the result with a select statement.
A superordinate procedure (= runner procedure) calls the worker procedures one after the other and collects their result in a table using an insert-into statement.
Processing within the worker procedures is secured with try-/catch statements so that the runner procedure is not aborted if one of the worker procedures should fail. It is also ensured that the worker procedures always return a result, even if this procedure encounters an error during processing.
However, the runner procedure encounters an error if an error has occurred in a worker procedure, although this error has already been intercepted and handled in the worker procedure.
The error message is: „The current transaction cannot be committed and cannot support operations that write to the log file. Roll back the transaction.“
As you can see from my example script, no explicit transactions are used there.
The problem only occurs when the results of the worker procedures are transferred to a table with a Select-Into statement. As an example I have created another runner procedure, which only executes the worker procedures, but does not transfer their results to a table. In this case there will be no errors.
CREATE SCHEMA bugchase;
GO
DROP PROCEDURE IF EXISTS bugchase.worker_1;
DROP PROCEDURE IF EXISTS bugchase.worker_2;
DROP PROCEDURE IF EXISTS bugchase.runner_selectOnly;
DROP PROCEDURE IF EXISTS bugchase.runner_selectAndInsert;
GO
CREATE PROCEDURE bugchase.worker_1
AS
BEGIN
-- this worker just returns a value
SELECT
'Result Worker 1';
END;
GO
CREATE PROCEDURE bugchase.worker_2
AS
BEGIN
-- this worker encounters an error while processing.
-- the error will be catched and some data will be returned
DECLARE #result INT;
BEGIN TRY
-- this will force an error, because 'ABCD' could not be casted as float
SET #result = CAST('ABCD' AS FLOAT);
END TRY
BEGIN CATCH
-- just catch, don't do anything else
END CATCH;
SELECT
'Result Worker 2';
END;
GO
CREATE PROC bugchase.runner_selectAndInsert
AS
BEGIN
SET NOCOUNT ON;
DECLARE #result AS TABLE (value NVARCHAR(64));
-- exec worker_1 (no error will be thrown inside worker_1)
PRINT 'Select&Insert: Worker 1 start';
INSERT INTO #result (value)
EXEC bugchase.worker_1;
PRINT 'Select&Insert: Worker 1 end';
-- exec worker_2 (will throw and catch an error inside)
PRINT 'Select&Insert: Worker 2 start';
INSERT INTO #result (value)
EXEC bugchase.worker_2;
PRINT 'Select&Insert: Worker 2 end';
END;
GO
CREATE PROC bugchase.runner_selectOnly
AS
BEGIN
SET NOCOUNT ON;
-- exec worker_1 (no error will be thrown inside worker_1)
PRINT 'SelectOnly: Worker 1 start';
EXEC bugchase.worker_1;
PRINT 'SelectOnly: Worker 1 end';
-- exec worker_2 (will throw and catch an error inside)
PRINT 'SelectOnly: Worker 2 start';
EXEC bugchase.worker_2;
PRINT 'SelectOnly: Worker 2 end';
END;
GO
BEGIN TRY
-- because all errors are catched within the worker-procedures, there should no error occur by this call
EXEC bugchase.runner_selectAndInsert;
END TRY
BEGIN CATCH
-- indeed an error will occur while running worker 2
PRINT CONCAT('Select&Insert: ERROR:', ERROR_MESSAGE());
END CATCH;
GO
BEGIN TRY
-- for demonstration only, this will run as expected
EXEC bugchase.runner_selectOnly;
END TRY
BEGIN CATCH
-- No error will occur
PRINT CONCAT('SelectOnly: ERROR:', ERROR_MESSAGE());
END CATCH;
GO
DROP PROCEDURE bugchase.worker_2;
DROP PROCEDURE bugchase.worker_1;
DROP PROCEDURE bugchase.runner_selectOnly;
DROP PROCEDURE bugchase.runner_selectAndInsert;
DROP SCHEMA bugchase;
The script above will show the following result:
Select&Insert: Worker 1 start
Select&Insert: Worker 1 end
Select&Insert: Worker 2 start
Select&Insert: ERROR:
The current transaction cannot be committed and cannot support operations that write to the log file.
Roll back the transaction.
No error occurs, when the results are not stored into a table:
SelectOnly: Worker 1 start
SelectOnly: Worker 1 end
SelectOnly: Worker 2 start
SelectOnly: Worker 2 end
As Anton mentioned in the comments, this is a side effect of the autocommit functionality of SQL Server which is mentioned in the documentation for SET IMPLICIT_TRANSACTIONS. Basically, with IMPLICIT_TRANSACTIONS OFF (the default), there are a bunch of statements that are automatically wrapped in an unseen transaction. INSERT is one of them, so anything that happens within the INSERT INTO EXEC is in one of these autocommit transactions. You can see this for yourself by printing or selecting ##trancount within one of the worker procs.
This other question has some great explanations of why the uncommittable transaction occurs even though you are catching the error.
In your case, you can work around this by rewriting the code to return data from the worker procs without using insert into. One simple option would be to replace your table variable in the runner proc with a temp table, then insert into that temp table within the worker procs instead of just selecting the result set.
e.g runner_selectAndInsert would become:
CREATE PROC bugchase.runner_selectAndInsert
AS
BEGIN
SET NOCOUNT ON;
CREATE TABLE #result (value NVARCHAR(64));
-- exec worker_1 (no error will be thrown inside worker_1)
PRINT 'Select&Insert: Worker 1 start';
EXEC bugchase.worker_1;
PRINT 'Select&Insert: Worker 1 end';
-- exec worker_2 (will throw and catch an error inside)
PRINT 'Select&Insert: Worker 2 start';
EXEC bugchase.worker_2;
PRINT 'Select&Insert: Worker 2 end';
END;
and worker_1 would become:
CREATE PROCEDURE bugchase.worker_1
AS
BEGIN
-- this worker just returns a value
INSERT INTO #result (value)
SELECT
'Result Worker 1';
END;
Some more options for passing data between procs like this are fleshed out by Erland Sommarskog here.
I'm using SQL Server 2014; my stored procedure will be nested transaction procedure, whereby it will call few stored procedures that have transaction in them. If either one of the inner stored procedures hits an error, then will rollback all, such as
Begin Try
Begin Tran
Exec Stored Proc 1 (with begin tran inside)
Exec Stored Proc 2 (with begin tran inside)
Exec Stored Proc 3 (with begin tran inside)
Exec Stored Proc 4 (with begin tran inside)
Exec Stored Proc 5 (with begin tran inside)
Commit Tran
End Try
Begin Catch
Catch exception then roll back tran
End Catch
The problem is the transaction count after execute the inner stored procedures are mismatched, however if I didn't open a transaction in the inner stored procedure, it won't rollback. Can anyone give me some suggestions?
Committing inner transactions is ignored by the SQL Server Database Engine. The transaction is either committed or rolled back based on the action taken at the end of the outermost transaction. If the outer transaction is committed, the inner nested transactions are also committed.
Nesting Transactions
I have following definition of stored procedure:
CREATE procedure dbo.ImportData
(
#SessionId VARCHAR(20)
,#ImportId int
)
as
begin
PRINT 'TRANCOUNT value = ' + CAST(##TRANCOUNT AS VARCHAR)
begin try
--business log
begin try
BEGIN CATCH
--business log
END CATCH
END
Whenever i am running my SP, i am getting ##Trancount value as 1 however i have not begin any transaction using BEGIN TRAN statement.
Please suggest this behavior of sql server of creating a transition after BEGIN statement of the SP definition.
Also, when i tried to replicate the same behavior by creating another SP without parameter and only one SELECT statement but i am getting Transition value as 0.
Please suggest this concept.
You have already opened transaction (maybe accidentally).
Try to disconnect from sql-server or commit (rollbcack) existing transactions.
I am wondering, if there is a possibility to apply conditional transaction rollback based on state of particular batch. For example, I have a following code:
BEGIN TRAN
--EXEC No 1
EXEC [dbo].[MyProc] 1;
GO
--EXEC No 2
EXEC [dbo].[MyProc] 22;
GO
--EXEC No 3
EXEC [dbo].[MyProc] 333;
GO
--EXEC No 4
EXEC [dbo].[MyProc] 5;
GO
COMMIT
And I want to rollback entire transaction if the EXEC No 3 fails. If any other execution fails I want SQL Server to continue executing my query. Is it possible?
Begin tran
Begin try
.....
commit
End try
begin catch
rollback
End catch
http://msdn.microsoft.com/fr-fr/library/ms175976.aspx
They prefer to do the begin tran and commit outside
Yes, this is possible. Use TRY/CATCH blocks around each procedure call, determine how to handle errors for each procedure in each respective CATCH block.
In your case, only perform a ROLLBACK in the CATCH block for the third procedure call.
I am in the process of creating a stored procedure. This stored procedure runs local as well as external stored procedures. For simplicity, I'll call the local server [LOCAL] and the remote server [REMOTE].
Here's a simple topology:
The procedure
USE [LOCAL]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER PROCEDURE [dbo].[monthlyRollUp]
AS
SET NOCOUNT, XACT_ABORT ON
BEGIN TRY
EXEC [REOMTE].[DB].[table].[sp]
--This transaction should only begin if the remote procedure does not fail
BEGIN TRAN
EXEC [LOCAL].[DB].[table].[sp1]
COMMIT
BEGIN TRAN
EXEC [LOCAL].[DB].[table].[sp2]
COMMIT
BEGIN TRAN
EXEC [LOCAL].[DB].[table].[sp3]
COMMIT
BEGIN TRAN
EXEC [LOCAL].[DB].[table].[sp4]
COMMIT
END TRY
BEGIN CATCH
-- Insert error into log table
INSERT INTO [dbo].[log_table] (stamp, errorNumber,
errorSeverity, errorState, errorProcedure, errorLine, errorMessage)
SELECT GETDATE(), ERROR_NUMBER(), ERROR_SEVERITY(), ERROR_STATE(), ERROR_PROCEDURE(),
ERROR_LINE(), ERROR_MESSAGE()
END CATCH
GO
When using a transaction on the remote procedure, it throws this error:
OLE DB provider ... returned message "The partner transaction manager has disabled its support for remote/network transactions.".
I get that I'm unable to run a transaction locally for a remote procedure.
How can I ensure that the this procedure will exit and rollback if any part of the procedure fails?
Notes
With regards to combining the simple procedures, some of them are used individually.
IMO easiest way is to
Add Return value to remote proc.
Wrap remote proc into transaction and try catch (inside remote proc). If error happened return false.
On local stored proc if false, simply do not continue.
I also fail to understand the reason behind multiple BEGIN TRANS / COMMIT in the local proc. I mean if this is month end rollup, shuldn't this be one big transaction rather than a bunch of small? Otherwise your trans 1 and 2 may commit successfully, but 3 will fail and that's that.
Names are made up ofc:
CREATE PROC [remote].db.REMOTE_PROC (
#return_value int output
)
AS
BEGIN
SET XACT_ABORT ON;
BEGIN TRY
BEGIN TRANS
... do stuff ...
set #return_value = 1;
COMMIT;
END TRY
BEGIN CATCH
set #return_value = 0;
END CATCH
END
and the local proc
CREATE PROC [local].db.[monthlyRollUp] AS
BEGIN
SET XACT_ABORT ON;
declare #ret int;
EXECUTE [remote].dbo.REMOTE_PROC #return_value = #ret OUTPUT;
IF #ret = 0
PRINT 'ERROR :('
RETURN
END IF
BEGIN TRANS
-- one big transaction here
EXEC [LOCAL].[DB].[table].[sp1];
EXEC [LOCAL].[DB].[table].[sp2];
EXEC [LOCAL].[DB].[table].[sp3];
EXEC [LOCAL].[DB].[table].[sp4];
COMMIT;
END;
afair [remote].dbo.REMOTE_PROC runs its own transaction space, and returns 1 if successful. Local proc, checks the return value and decides whether to proceed or not.
sp1 sp2 sp3 and sp4 are all running in one single transactions, as having multiple transactions for each of them does not really make much sense to me.
You can try to execute both stored procedure into seperate TRY CATCH block and check for corresponding ERROR_NUMBER in CATCH block. If ERROR_NUMBER is same as error you are getting you can simply return or raiseerror as per your requirement.
Is it causing a fatal error. Please check what error severity is in the exception.
I might be a little unclear on what you want. If you need the entire monthlyRollUp SP to rollback on a failure of either the remote or local procedures, then you will need a distributed transaction coordinator. This will allow the servers to communicate the information about the transaction and coordinate the commits. I.e., both servers have to indicate that all necessary locks were gained and then coordinate commits on both servers so that the operation is automic. Here is one example of setting up a DTC:
http://social.msdn.microsoft.com/forums/en-US/adodotnetdataproviders/thread/7172223f-acbe-4472-8cdf-feec80fd2e64/
If you don't want the remote procedures to participate/affect the transaction, you can try setting:
SET REMOTE_PROC_TRANSACTIONS OFF;
http://msdn.microsoft.com/en-us/library/ms178549%28SQL.90%29.aspx
I haven't used that setting before though so I'm not sure if it will accomplish what you need.
If you can't or don't want to use DTC, and don't want to use CLR, then then you need to call the remote sp last, as you won't be able to rollback the remote sp call.
SET NOCOUNT, XACT_ABORT ON
SET REMOTE_PROC_TRANSACTIONS OFF;
BEGIN TRY
DECLARE #ret INT
BEGIN TRAN
--Perform these in a transaction, so they all rollback together
EXEC [LOCAL].[DB].[table].[sp1]
EXEC [LOCAL].[DB].[table].[sp2]
EXEC [LOCAL].[DB].[table].[sp3]
EXEC [LOCAL].[DB].[table].[sp4]
--We call remote sp last so that if it fails we rollback the above transactions
--We'll have to assume that remote sp takes care of itself on error.
EXEC [REMOTE].[DB].[table].[sp]
COMMIT
END TRY
BEGIN CATCH
--We rollback
ROLLBACK
-- Insert error into log table
INSERT INTO [dbo].[log_table] (stamp, errorNumber,
errorSeverity, errorState, errorProcedure, errorLine, errorMessage)
SELECT GETDATE(), ERROR_NUMBER(), ERROR_SEVERITY(), ERROR_STATE(),ERROR_PROCEDURE(),
ERROR_LINE(), ERROR_MESSAGE()
END CATCH
If the local sp's depend on results from the remote stored procedure, then you can use a CLR sp (will need EXTERNAL_ACCESS permissions) and manage the transactions explicitly (basically, a roll your own DTC, but no two-phase commit. You're effectively delaying the remote commit.)
//C# fragment to roll your own "DTC" This is not true two-phase commit, but
//may be sufficient to meet your needs. The edge case is that if you get an error
//while trying to commit the remote transaction, you cannot roll back the local tran.
using(SqlConnection cnRemote = new SqlConnection("<cnstring to remote>"))
{
try {
cnRemote.Open();
//Start remote transaction and call remote stored proc
SqlTransaction trnRemote = cnRemote.BeginTransaction("RemoteTran");
SqlCommand cmdRemote = cnRemote.CreateCommand();
cmdRemote.Connection = cnRemote;
cmdRemote.Transaction = trnRemote;
cmdRemote.CommandType = CommandType.StoredProcedure;
cmdRemote.CommandText = '[dbo].[sp1]';
cmdRemote.ExecuteNonQuery();
using(SqlConnection cnLocal = new SqlConnection("context connection=true"))
{
cnLocal.Open();
SqlTransaction trnLocal = cnLocal.BeginTransaction("LocalTran");
SqlCommand cmdLocal = cnLocal.CreateCommand();
cmdLocal.Connection = cnLocal;
cmdLocal.Transaction = trnLocal;
cmdLocal.CommandType = CommandType.StoredProcedure;
cmdLocal.CommandText = '[dbo].[sp1]';
cmdLocal.ExecuteNonQuery();
cmdLocal.CommandText = '[dbo].[sp2]';
cmdLocal.ExecuteNonQuery();
cmdLocal.CommandText = '[dbo].[sp3]';
cmdLocal.ExecuteNonQuery();
cmdLocal.CommandText = '[dbo].[sp4]';
cmdLocal.ExecuteNonQuery();
//Commit local transaction
trnLocal.Commit();
}
//Commit remote transction
trnRemote.Commit();
} // try
catch (Exception ex)
{
//Cleanup stuff goes here. rollback remote tran if needed, log error, etc.
}
}