We have a service broker activation sp for an auditing service that we are in the process of testing. Our QA purposefully en-queued a message with a bad XML element so it fails during the XML parsing process (does not get to the point of inserting an audit record). In our catch block, we attempt to do a rollback to the save point after the message is received (so it won't be re-queued and eventually poison the queue) but sql gives us the following error when we do this:
"The current transaction cannot be committed and cannot be rolled back to a save-point. Roll back the entire transaction."
Why does SQL prevent us from rolling back to the save point in this instance? Is there a good workaround?
ALTER PROCEDURE [dbo].[ImageAuditMessageDequeue]
AS
SET NOCOUNT ON;
DECLARE @RecvReqDlgHandle UNIQUEIDENTIFIER;
DECLARE @RecvReqMsg varbinary(max);
DECLARE @RecvReqMsgName sysname;
declare @payload xml;
DECLARE @ErrorNumber AS INT;
DECLARE @ErrorMessage as nvarchar(4000);
DECLARE @ErrorLine as int;
DECLARE @ErrorProcedure as nvarchar(128);
DECLARE @ErrorState as int;
DECLARE @ErrorSeverity as int;
DECLARE @EventMessage as VARCHAR(MAX);
WHILE (1=1)
BEGIN
BEGIN TRANSACTION;
WAITFOR
( RECEIVE TOP(1)
@RecvReqDlgHandle = conversation_handle,
@RecvReqMsg = message_body,
@RecvReqMsgName = message_type_name
FROM ImageAuditQueue
), TIMEOUT 5000;
IF (@@ROWCOUNT = 0)
BEGIN
ROLLBACK TRANSACTION;
BREAK;
END
SAVE TRANSACTION MessageReceivedSavePoint
select @payload = cast(@RecvReqMsg as xml);
IF @RecvReqMsgName = N'http://schemas.foo.com/messages/ImageAudit/Insert'
BEGIN
BEGIN TRY
--Ugh, SQL XQuery will return 0 (instead of null) for empty node values that are declared as int so we must workaround this to coerce a null value for empty nodes
declare @companyId as int = cast(nullif(@payload.value('(/Root/ImageAudit/CompanyID)[1]', 'varchar(25)'), '') AS int);
declare @appId as int = @payload.value('(/Root/ImageAudit/AppID)[1]','int'); --error raised here due to bad xml value in message
declare @batchId as char(6) = @payload.value('(/Root/ImageAudit/BatchID)[1]','char(6)');
declare @imageId as int = cast(nullif(@payload.value('(/Root/ImageAudit/ImageID)[1]', 'varchar(25)'), '') AS int);
declare @docId as int = cast(nullif(@payload.value('(/Root/ImageAudit/DocID)[1]', 'varchar(25)'), '') AS int);
declare @docAge as int = cast(nullif(@payload.value('(/Root/ImageAudit/DocAgeInDays)[1]', 'varchar(25)'), '') AS int);
declare @currentDateTime as datetime = @payload.value('(/Root/ImageAudit/CurrentDateTime)[1]','datetime');
declare @userId as int = @payload.value('(/Root/ImageAudit/UserID)[1]','int');
declare @action as varchar(25) = @payload.value('(/Root/ImageAudit/Action)[1]','varchar(25)');
declare @seconds as float = cast(nullif(@payload.value('(/Root/ImageAudit/Seconds)[1]', 'varchar(25)'), '') AS float);
declare @additionalInfo as varchar(max) = @payload.value('(/Root/ImageAudit/AdditionalInfo)[1]','varchar(max)');
INSERT INTO FOTNAudit.dbo.ImageAuditDaily
(AppID, BatchID, ImageID, DocID, CurrentDateTime, UserID, Action, Seconds,CompanyID, DocAgeInDays, AdditionalInfo)
VALUES
(@appId, @batchId, @imageId, @docId, @currentDateTime, @userId, @action, @seconds, @companyId, @docAge, @additionalInfo)
END CONVERSATION @RecvReqDlgHandle;
END TRY
BEGIN CATCH
-- We do not want to allow a message insertion to fail 5 consecutive times otherwise we risk the queue being stopped / poisoned.
--Because of this we implement our own error handling that simply logs the error and ends the conversation.
--This SHOULD prevent failed insertions from ever poisoning the queue.
SET @ErrorNumber = ERROR_NUMBER();
SET @ErrorSeverity = ERROR_SEVERITY();
SET @ErrorState = ERROR_STATE();
SET @ErrorProcedure = ERROR_PROCEDURE();
SET @ErrorLine = ERROR_LINE();
SET @ErrorMessage = ERROR_MESSAGE();
SET @EventMessage = CONVERT(VARCHAR(MAX), @payload);
EXEC xp_logevent 60000, @EventMessage, ERROR; --this guy is not transactional so don't need to worry about logevent rollback
PRINT '@EventMessage = ' + @EventMessage
PRINT '@ErrorMessage = ' + @ErrorMessage
--Roll back transaction to the point that message has been deqeued but not processed
--to avoid potential queue poisoning
--This is where encounter the following error:
--"The current transaction cannot be committed and cannot be rolled back to a savepoint. Roll back the entire transaction."
rollback transaction MessageReceivedSavePoint;
--end conversation @RecvReqDlgHandle with error = @ErrorNumber description = @ErrorMessage;
--No real benefit in signaling a failure to sender so we just end the conversation
END CONVERSATION @RecvReqDlgHandle;
END CATCH;
END
ELSE IF @RecvReqMsgName = N'http://schemas.microsoft.com/SQL/ServiceBroker/EndDialog'
BEGIN
END CONVERSATION @RecvReqDlgHandle;
END
ELSE IF @RecvReqMsgName = N'http://schemas.microsoft.com/SQL/ServiceBroker/Error'
BEGIN
--Handling Service Broker Error Messages
--https://technet.microsoft.com/en-us/library/ms171599(v=sql.105).aspx
--Log an error to the sql server log file and event viewer
SET @EventMessage = CONVERT(VARCHAR(MAX), @RecvReqMsg);
EXEC xp_logevent 60000, @EventMessage, ERROR;
PRINT '@EventMessage = ' + @EventMessage
END CONVERSATION @RecvReqDlgHandle;
END
COMMIT TRANSACTION;
END
SQL2014