Return if remote stored procedure fails - sql-server

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.
}
}

Related

Executing a stored procedure fails when placed in transaction

I have a query that I want to execute via a linked server. The query looks like this:
USE db1;
SET xact_abort ON;
DECLARE #statement NVARCHAR(max);
SET #statement = 'EXECUTE (''INSERT INTO T1(V1, V2) VALUES (1, 2)'') AT LS1';
BEGIN try
BEGIN TRANSACTION
EXEC Sp_executesql #statement
COMMIT TRANSACTION
END try
BEGIN catch
IF ( Xact_state() ) = -1
BEGIN
PRINT Error_message()
ROLLBACK TRANSACTION
END
IF ( Xact_state() ) = 1
BEGIN
PRINT 'COMMIT OPEN TRANSACTION'
COMMIT TRANSACTION
END
INSERT INTO tblerrmsg (errornumber, errorseverity, errorstate, errorline, errormessage) EXECUTE Usp_geterrorinfo;
END catch
This fails with an entry in my TblErrMsg table.
ErrorNumber = 8501, ErrorSeverity = 16, ErrorState = 3, ErrorLine = 1, ErrorMessage = MSDTC on server 'XXX' is unavailable.
So I researched for the specific error message and checked if the Distributed Transaction Coordinator Service was running on the server, but this was already the case. Even a restart of the service did not bring any change. Next I tried to remove the transaction and execute the following procedure:
USE db1;
DECLARE #statement NVARCHAR(max);
SET #statement = 'EXECUTE (''INSERT INTO T1(V1, V2) VALUES (1, 2)'') AT LS1';
BEGIN try
EXEC Sp_executesql #statement
END try
BEGIN catch
PRINT Error_message()
END catch
And this time it worked. There were no errors and the INSERT also worked. So I'm wondering what really the problem is. Apparently there seems to be no problem with the execution of the procedure nor with the Linked Server connection.
Has anyone ever had a similar problem, or has an explanation for this behavior?
It's possible you're missing the key word DISTRIBUTED when specifying the explicit transaction.
Instead of
BEGIN TRANSACTION
Try
BEGIN DISTRIBUTED TRANSACTION
According to the Docs this is the way to specify "... the start of a Transact-SQL distributed transaction managed by Microsoft Distributed Transaction Coordinator (MS DTC)"
Without the explicit transaction the default behavior afaik is the transaction across linked server is not guaranteed to be atomic and cannot be rolled back. This why it works when no explicit transaction is specified.
When BEGIN TRANSACTION is specified without 'DISTRIBUTED' the statement is asking something that's not possible when the procedure contains a linked server reference.

NServiceBus handler and stored procedure call. Should I use SET XACT_ABORT ON or TRY CATCH?

I want to call my ms sql stored procedure from nservicebus handler and I wonder if I still need to use something like
CREATE PROCEDURE [dbo].[proc_fd_SomeEntitySyncRawWithStage]
AS
BEGIN
BEGIN TRY
BEGIN TRANSACTION
-- Batch of inserts and updates that I want to keep transactional
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION;
DECLARE #ErrorMessage NVARCHAR(4000);
DECLARE #ErrorSeverity INT;
DECLARE #ErrorState INT;
SELECT
#ErrorMessage = ERROR_MESSAGE(),
#ErrorSeverity = ERROR_SEVERITY(),
#ErrorState = ERROR_STATE();
RAISERROR (#ErrorMessage,
#ErrorSeverity,
#ErrorState
);
END CATCH;
END
Do I really need all this TRY BEGIN TRAN... CATCH ROLLBACK .. or SET XACT_ABORT ON... stuff to rollback my transaction or NServiceBus will rollback it in case of any sql exception in message handler ?
P.S.
Keeping in mind that:
"If a run-time statement error (such as a constraint violation) occurs in a batch, the default behavior in the Database Engine is to roll back only the statement that generated the error"
and
"When SET XACT_ABORT is OFF, in some cases only the Transact-SQL statement that raised the error is rolled back and the transaction continues processing."
Normally, you should always wrap your procedure calls in a transaction, would you use ORM or ADO.NET. Wht ADO.NET you would do something like
var ts = myConnection.BeginTransaction();
and then call your procedure within a try-catch block.
Further depends on your logic. If you procedure could fail because of some logic, you will need to have a return code and throw an exception based on that. If your procedure just fails with SQL exception, the try-catch block will handle it.
In the catch block you need to rollback the transaction.
If you re-throw your exception in the catch block, the message will be retried by FLR and if configured - by SLR. Usually, this helps when having deadlicks in the database, FLR normally handles this.

Are these code snippets equivalent ('set xact_abort on' vs 'try catch rollback')?

I used to use this code snippet within my stored procedure in SQL Server:
create procedure proc_name
--declare variables
as
set nocount on
begin transaction
begin try
--do something
commit transaction
end try begin catch
rollback transaction
;throw
end catch
go
but today I got to know 'set xact_abort on' statement.
Is the following code equivalent to previous one? Are there any differences between them?
create procedure proc_name
--declare variables
as
set nocount on
set xact_abort on
begin transaction
--do something
commit transaction
go
Quoting from MS docs
A TRY…CATCH construct catches all execution errors that have a severity higher than 10 that do not close the database connection.
So, try catch does not catch all possible errors. You can use xact_abort on in addition to try catch.
try/catch give you more flexibility, i.e., you are not limited to just a rollback when something is not happy.

save point within a transaction and store error message in sql server table

I have stored procedure which is for inserting data into a table. This procedure is called from asp.net application which handles the transaction start, commit and rollback functionality. Inside the stored procedure there is no transaction.
In this scenario my application is working fine and it is hosted in the live. Now, inside the store procedure, I have to add a new functionality to insert another table by linked server to another database and if error appears then I have to store it in the database.
We want to implement this insertion in such a way so that the previous sp will be working fine.
Noted that if error from the insertion of linked server comes then entire process is rolled back and also save point is not working. We can do this by dot net code but this is for more 40 modules, so I have to do this by sp . So, how can we implement this.
MSDN SAVE TRANSACTION
CREATE PROCEDURE [dbo].[SaveCustomer]
#Firstname nvarchar(50),
#Lastname nvarchar(50)
AS
DECLARE #ReturnCode int = 1 -- 1 - success
,#NewID int
---------------------------------------------- These variables are for TRY/CATCH RAISEERROR and BEGIN TRAN/SAVE POINT use
,#tranCounter int -- #TranCounter > 0 means an active transaction was started before the procedure was called.
,#errorMessage nvarchar(4000) -- echo error information to the caller. Message text.
,#errorSeverity int -- Severity.
,#errorState int; -- State
SET #tranCounter = ##TRANCOUNT;
IF #tranCounter > 0
SAVE TRANSACTION SaveCustomer_Tran;
ELSE
BEGIN TRANSACTION;
BEGIN TRY
INSERT dbo.Customer (
Firstname
,Lastname)
VALUES (
#Firstname
,#Lastname)
SET #NewID = scope_identity()
IF #tranCounter = 0
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF #tranCounter = 0
ROLLBACK TRANSACTION;
ELSE
IF XACT_STATE() <> -1
ROLLBACK TRANSACTION SaveCustomer_Tran;
SELECT #errorMessage = 'Error in [SaveCustomer]: ' + ERROR_MESSAGE(), #errorSeverity = ERROR_SEVERITY(), #errorState = ERROR_STATE();
RAISERROR (#errorMessage, #errorSeverity, #errorState);
SET #ReturnCode = -1
END CATCH
SELECT #ReturnCode as [ReturnCode], #NewID as [NewID]
GO
There are two accepted ways to escape the current transaction rollback tarpit.
One way is to use a looopback connection. Either via linked server, or from SQLCLR, connect back to the current server and write the INSERT, making sure DTC enrollment is not allowed. As the loopback connection is a different transaction, you can safely rollback in the original transaction and preserve the INSERT. This approach has the drawback that is very easy to deadlock yourself.
The other, less known, way is to use sp_trace_generateevent to fire off a user-configurable event. This may not seem much, but you can create event notifications for these events and then impelemnt handling that does the INSERT in processing the event. Eg. see Sql progress logging in transaction. This approach has the drawback of being difficult.

How to rollback a transaction in a stored procedure?

Looking at the SQL Server Books Online, Microsoft seems to have an (incorrect) method of handling nested transactions in a stored procedure:
Nesting Transactions
Explicit transactions can be nested. This is primarily intended to support transactions in stored procedures that can be called either from a process already in a transaction or from processes that have no active transaction.
The example goes on to show a stored procedure that starts its own transaction ("The procedure enforces its transaction regardless of the transaction mode of any process that executes it."):
CREATE PROCEDURE TransProc #PriKey INT, #CharCol CHAR(3) AS
BEGIN TRANSACTION InProc
...
COMMIT TRANSACTION InProc;
This procedure can then either be called without a transaction running:
EXECUTE TransProc 3,'bbb';
Or with an explicit transaction:
BEGIN TRANSACTION OutOfProc;
EXEC TransProc 1, 'aaa';
COMMIT TRANSACTION OutOfProc
What they don't address is what happens when the stored produre:
fails with an error, but leaves the transaction running
fails with an error, but doesn't leave the transaction running
encounters an error, but continues executing with the transaction open
encounters an error, but continues executing with the transaction rolled back
There is no:
SET XACT_ABORT ON
##TRANCOUNT
anywhere in the canonical example.
If i didn't know any better, i would have thought that the line:
The following example shows the intended use of nested transactions.
should actually read
The following example shows the how not to use nested transactions.
Unless someone can make heads or tails of this BOL example?
You need to use the try catch block with the transaction. So in case you get the error in your catch block then you can rollback your transaction.
Please see the below sql server code for that.
BEGIN TRANSACTION;
BEGIN TRY
-- Some code
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION;
END CATCH;
CREATE PROCEDURE [usp_my_procedure_name]
AS
BEGIN
SET NOCOUNT ON;
DECLARE #trancount int;
SET #trancount = ##trancount;
BEGIN TRY
IF #trancount = 0
BEGIN TRANSACTION
ELSE
SAVE TRANSACTION usp_my_procedure_name;
-- Do the actual work here
lbexit:
IF #trancount = 0
COMMIT;
END TRY
BEGIN CATCH
DECLARE #error int,
#message varchar(4000),
#xstate int;
SELECT
#error = ERROR_NUMBER(),
#message = ERROR_MESSAGE(),
#xstate = XACT_STATE();
IF #xstate = -1
ROLLBACK;
IF #xstate = 1 AND #trancount = 0
ROLLBACK
IF #xstate = 1 AND #trancount > 0
ROLLBACK TRANSACTION usp_my_procedure_name;
RAISERROR ('usp_my_procedure_name: %d: %s', 16, 1, #error, #message);
END CATCH
END

Resources