Building on the topics previously addressed by gbn in these questions
Q1, Q2, Q3, Q4, and regarding the use of synonyms and re-creating the synonyms to keep the synonyms pointing at live data, it is not clear to me how prevent a "race condition" by using
"sp_getapplock after BEGIN TRAN in Transaction mode and trap/handle the return status as required."
MSDN documentation for sp_getapplock is a bit cryptic for me. For instance, can resource_name be any made-up string? But more to the point: if I run a single proc containing nested procs, where the first step is to build tables, and if those succeed, the next major step is to DROP and CREATE the existing synonyms, how would I properly implement sp_getapplock?
CREATE PROCEDURE [dbo].[some_old_proc]
AS
SET XACT_ABORT, NOCOUNT ON
DECLARE #nested_build_success varchar(3) = 'No'
DECLARE #starttrancount int
BEGIN TRY
SET #starttrancount = ##TRANCOUNT
IF #starttrancount = 0
BEGIN TRANSACTION
-- fill the tables that the synonyms don't point to yet...
EXEC dbo.nested_proc_1
EXEC dbo.nested_proc_2
EXEC dbo.nested_proc_3
EXEC dbo.nested_proc_4
EXEC dbo.nested_proc_5
IF #starttrancount = 0
BEGIN
COMMIT TRANSACTION
SET #nested_build_success = 'Yes'
END
END TRY
BEGIN CATCH
IF XACT_STATE() <> 0 AND #starttrancount = 0
ROLLBACK TRANSACTION
-- RAISERROR... log error event
END CATCH
IF #nested_build_success = 'Yes'
BEGIN TRAN
-- simple talk article
-- http://www.simple-talk.com/sql/t-sql-programming/developing-modifications-that-survive-concurrency/
DECLARE #ret INT -- does it matter what the resource_name is?
EXEC #ret = sp_getapplock #Resource = 'DoesNameMatterHere', #LockMode = 'Exclusive';
IF #ret < 0
BEGIN
-- log error message?
END
ELSE
BEGIN
-- call the proc that a does a DROP and CREATE of the relevant synonyms
-- so the synonyms point at a different set of tables...
EXEC dbo.change_the_synonyms
END
COMMIT TRAN
Perhaps a different and better way exists to avoid a race condition than the use of sp_getapplock, or a good example of what I'm trying to do is available?
If I understand your question correctly the preparation steps and the synonym setup must be in a single transaction, not two separate transactions. Here is an example, based on the Exception handling and nested transactions template:
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;
EXEC sp_getapplock 'usp_my_procedure_name',
'Exclusive',
'TRANSACTION';
EXEC dbo.nested_proc_1;
EXEC dbo.nested_proc_2;
EXEC dbo.nested_proc_3;
EXEC dbo.nested_proc_4;
EXEC dbo.nested_proc_5;
EXEC dbo.change_the_synonyms;
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
This will do all the work atomically. It will use an applock to serialize access so that no two procedures execute this work concurrently. In case of error the work will either completely roll back or, in the case when the caller already has a transaction, it rolls back the work to a consistent state at the procedure entry w/o rolling back the caller (this is extremely useful in batch processing). XACT_ABORT has its use in deployment script, but mixing XACT_ABORT with TRY/CATCH is a big no-no in my book.
Related
I have one stored procedure proc_in which the insert data to tbl table
create table tbl(id int identity, val nvarchar(50))
create procedure proc_in
as
begin
insert into tbl(val)
values ('test')
end
and I have proc_out where I call proc_in
create procedure proc_out
as
begin
exec proc_in
DECLARE #MessageText NVARCHAR(100);
SET #MessageText = N'This is a raiserror %s';
RAISERROR(#MessageText, 16, 1, N'MSG')
end
How I can write proc_out that it return raiserror always to do insert in TBL table.
I calling proc_out like this
begin tran
declare #err int = 0
exec #err = proc_out
if #ERR = 0
commit tran
else
rollback tran
You are wrapping your call in a single transaction in the calling context, therefore:
begin tran
declare #err int = 0
exec #err = proc_out
if #ERR = 0
commit tran
else
rollback tran
will always roll back everything that has happened within that transaction.
One way to avoid this is to move the transaction inside your 'proc_out' SP e.g.
create procedure proc_out
as
begin
set nocount, xact_abort on;
exec proc_in;
begin tran;
-- All your other code
if #Err = 1 begin
rollback;
declare #MessageText nvarchar(100);
set #MessageText = N'This is a raiserror %s';
--raiserror(#MessageText, 16, 1, N'MSG');
-- Actually for most cases now its recommended to use throw
throw 51000, #MessageText 1;
end; else begin
commit;
end;
return 0;
end;
Alternatively, and I haven't tried this, you could try using a savepoint e.g.
create procedure proc_out
as
begin
set nocount on;
exec proc_in;
save transaction SavePoint1;
declare #MessageText nvarchar(100);
set #MessageText = N'This is a raiserror %s';
raiserror(#MessageText, 16, 1, N'MSG');
return 0;
end;
Then call it as:
begin tran;
declare #err int = 0;
exec #err = proc_out;
if #ERR = 0;
commit tran;
end; else begin
rollback tran SavePoint1;
commit tran;
end;
I don't like this approach though, because knowledge of the inner workings of your SP has now leaked out to the calling context.
And some errors will roll back the entire transaction regardless.
Its important to be aware of the XACT_ABORT setting here.
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. Depending upon the severity of the error, the entire transaction may be rolled back even when SET XACT_ABORT is OFF. OFF is the default setting in a T-SQL statement, while ON is the default setting in a trigger.
Here is a shell of my stored procedure with the necessary parts not omitted:
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE procedure --name of sproc
--declare sproc params
AS
BEGIN
SET XACT_ABORT ON
SET NOCOUNT ON
BEGIN TRY
BEGIN TRANSACTION
--declare a few vars
--declare some table variables
--do some work
IF (--some condition here)
BEGIN
--actually do the work
END
ELSE
BEGIN
ROLLBACK TRANSACTION
SET #error = 'some value cannot be NULL'
RAISERROR(#error, 16, 1)
RETURN #error
END
COMMIT
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION
SELECT #error = ERROR_NUMBER()
, #message = ERROR_MESSAGE()
, #severity = ERROR_SEVERITY()
, #state = ERROR_STATE()
RAISERROR(#message, #severity, #state)
RETURN #error
END CATCH
END
GO
I am getting a deadlock error (which is not the subject of this post) in the "--actually do some work" section, and then the "Transaction count..." error is thrown.
Is my COMMIT in the wrong place?
Move the Begin Transaction above the Begin Try. If the try fails and jumps to the catch, everything initialized in the try falls out of scope. Beginning the transaction outside the scope of the try/catch makes it available to both the try and the catch block.
I have long script which contains the Create tables, create schemas,insert data,update tables etc.I have to do this by only on script in batch wise.I ran it before but it created every time some error due to this some object will present inside the database. So In need some mechanism which can handle the batch execution if something goes wrong the whole script should be rolled back.
Appreciated Help and Time.
--343
Try this:
DECLARE #outer_tran int;
SELECT #outer_tran = ##TRANCOUNT;
-- find out whether we are inside the outer transaction
-- if yes - creating save point if no starting own transaction
IF #outer_tran > 0 SAVE TRAN save_point ELSE BEGIN TRAN;
BEGIN TRY
-- YOUR CODE HERE
-- if no errors and we have started own transaction - commit it
IF #outer_tran = 0 COMMIT;
END TRY
BEGIN CATCH
-- if error occurred - rollback whole transaction if it is own
-- or rollback to save point if we are inside the external transaction
IF #outer_tran > 0 ROLLBACK TRAN save_point ELSE ROLLBACK;
--and rethrow original exception to see what happens
DECLARE
#ErrorMessage nvarchar(max),
#ErrorSeverity int,
#ErrorState int;
SELECT
#ErrorMessage = ERROR_MESSAGE() + ' Line ' + cast(ERROR_LINE() as nvarchar(5)),
#ErrorSeverity = ERROR_SEVERITY(),
#ErrorState = ERROR_STATE();
RAISERROR (#ErrorMessage, #ErrorSeverity, #ErrorState);
END CATCH
While I might not have caught all the nuances of your question, I believe XACT_ABORT will deliver the functionality you seek. Simply add a
SET XACT_ABORT ON;
to the beginning of your script.
With the 2005 release of SQL Server, you have access to try/catch blocks in TSQL as well.
I have a stored procedure in SQL Server with try catch. What I want to do in the catch loop is to call my own Stored procedure for logging with all the error variables, like so:
BEGIN TRY
-- Generate a divide-by-zero error.
SELECT 1/0;
END TRY
BEGIN CATCH
exec log.LogError ERROR_NUMBER(), ERROR_SEVERITY(), ERROR_MESSAGE();
END CATCH;
When I run this I get an error on the parenthesis.
I can run:
select ERROR_NUMBER(), ERROR_SEVERITY(), ERROR_MESSAGE();
I can also do
print ERROR_NUMBER()
What I want to do is the have just one line which calles the stored procedure with the parameters because I will have this in many stored procedures and don't want to have lot's of code setting the error parameters (I will have more than these three) in each stored procedure where I have try-catch.
Does anybody know how I can pass these into another stored procedure?
Regards,
Johann
Unfortunately T-SQL is not a DRY code reuse compact syntax programmer friendly language. You have to do it the hard way, and that implies writing a minimum of 4-5 lines of code inside each CATCH block. Besides, you need to account also for transaction semantics: has it rolled back or not? Or worse, are you in a doomed transaction? That's why I created this T-SQL error handling template:
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) ;
return;
end catch
end
Is it longer than what you're looking for? I bet. It is correct? Yes.
And finally, how do you handle logging in a transactional environment? Inserts into a log table will be rolled back along with everything else in case of error. Sometimes that is OK, other times is even desired, but sometimes is problematic. One of the most interesting solutions is Simon Sabin's Logging messages during a transaction.
Try changing your log.LogError procedure so it accesses ERROR_NUMBER() and the other error functions directly. There's an example in the documentation.
At the moment I write stored procedures this way:
create proc doStuff
#amount int
as
begin try
begin tran
...
if something begin select 'not_good' rollback return end
if someelse begin select 'some_other_thing' rollback return end
--do the stuff
...
commit
end try
begin catch
if ##trancount > 0 rollback
select 'error'
end catch
the problem with this approach is that I hide the error, anybody knows to do this some other ?
What database are you using? In SQL Server you can use the keyword RASIERROR to generate error messages. See RAISERROR (Transact-SQL)
Assuming SQL Server here, since that looks a lot like SQL Server syntax:
Preferably, you should also use SAVE TRAN so you can treat the procedure as its own unit of work and let the caller choose whether or not to rollback the entire transaction (as opposed to only rolling back the work in this particular block). Remus Rusanu wrote an article about that a while back.
Putting that aside for the moment, you need to save the error immediately after you catch it and then re-raise it after rolling back (normally with some additional info):
CREATE PROCEDURE xyz [params]
AS
BEGIN
BEGIN TRY
BEGIN TRAN
-- Do the work
COMMIT
END TRY
BEGIN CATCH
DECLARE
#Error int,
#Severity int,
#Message varchar(4000)
SELECT
#Error = ERROR_NUMBER(),
#Severity = ERROR_SEVERITY(),
#Message = ERROR_MESSAGE()
ROLLBACK
RAISERROR('Procedure xyz: %d: %s', #Severity, 1, #Error, #Message)
END CATCH
END
SQL server sp.
create procedure doStuff
(
#amount int
)
as
begin try
begin transaction
if something
raiserror('not_good', 16, 1)
if somethingelse
raiserror('some_other_thing', 16, 1)
do stuff here
commit
end try
begin catch
if ##trancount > 0
rollback
declare #errmsg nvarchar(4000), #errseverity int
select #errmsg = error_message(), #errseverity = error_severity()
raiserror(#errmsg, #errseverity, 1)
end catch