从 SQL 代理作业调用时工作存储过程失败
Working stored procedure fails when called from SQL Agent Job
我有一个存储过程,它在 SQL Server Management Studio 中运行良好,没有任何错误。但是,当相同的存储过程作为 SQL 代理作业的一个步骤执行时,它终止于:
Error 3930: The current transaction cannot be committed and cannot support operations that write to the log file. Roll back the transaction.
存储过程位于名为 [Billing]
的架构中。它使用的大多数表也在 [Billing]
架构中。
主存储过程开始一个数据库事务。主存储过程调用的存储过程在继承的事务上完成它们的所有工作。主存储过程负责提交或回滚事务。
数据库用户 运行 SQL 代理作业步骤不在系统管理员角色中,也不是 dbo。它属于 db_datareader 和 db_datawriter 数据库角色,并在 [Billing]
架构中被授予删除、执行、插入、引用、Select 和更新权限。
这是主要的存储过程:
CREATE PROCEDURE [Billing].[GenerateBillXml]
@pUseProductionPsSystem bit
,@pPeriodYear int
,@pPeriodMonthNumber int
,@pDocumentTypeName varchar(20)
,@pUser varchar(100)
,@pFilePath varchar(500)
,@pExportDateTime datetime2(7)
,@pResultCode int = 0 OUTPUT
AS
BEGIN
SET NOCOUNT ON;
SET @pResultCode = 0;
DECLARE @transactionBegun bit = 0
,@RC int = 0
,@processDateTimeUtc datetime2(7) = GETUTCDATE()
,@billGenerationId int
,@PsJobSuffix char(2)
,@periodDate date
,@periodDateString char(6)
,@PsBaseJobNumber char(6)
,@okayToRun int = 0
,@msg varchar(500);
BEGIN TRY
/* calculate the period date */
SET @periodDate = CONVERT(date,CAST(@pPeriodMonthNumber as varchar) + '/01/' + CAST(@pPeriodYear as varchar))
SET @periodDateString = CONVERT(varchar, @periodDate, 12); -- yyMMdd
/* retrieve the job suffix */
SELECT @PsJobSuffix = CAST([PsJobSuffix] as char(2))
FROM [dbo].[DocumentType] dt
WHERE [Name] like @pDocumentTypeName;
/* build the base job number */
SET @PsBaseJobNumber = LEFT(@periodDateString, 4) + @PsJobSuffix
/*
* We've made it past the input check - record the fact that we're generating a bill
*/
INSERT [Billing].[BillGeneration] (
[PsBaseJobNumber], [Status], [RunBy], [ProcessDateTimeUtc]
) VALUES (
@PsBaseJobNumber, 'Running', @pUser, @processDateTimeUtc
);
IF @@ROWCOUNT = 1
SET @billGenerationId = SCOPE_IDENTITY();
EXECUTE @RC = [Billing].[_0_OkayToGenerateBill]
@PsBaseJobNumber
,@okayToRun OUTPUT
,@pResultCode OUTPUT;
IF @pResultCode = 0
BEGIN
-- called stored procedure completed without error
IF @okayToRun = -1 -- this bill has already been generated
BEGIN
SET @msg = 'The billing for job ' + CAST(@PsBaseJobNumber as varchar) + ' has already been produced.';
RAISERROR(@msg, 16, 1)
END
IF @okayToRun = -2 -- too early to run billing for this period
BEGIN
SET @msg = 'It is too early to generate billing for job ' + CAST(@PsBaseJobNumber as varchar) + '.';
RAISERROR(@msg, 16, 1)
END
IF @okayToRun <> 1 -- unknown error...
BEGIN
SET @msg = 'Unknown error occured while determining whether okay to generate bill for job ' + CAST(@PsBaseJobNumber as varchar) + '.';
RAISERROR(@msg, 16, 1)
END
END
ELSE
BEGIN
SET @msg = 'Unknown failure in sub-stored procedure [Billing].[_0_OkayToRun]() for job ' + CAST(@PsBaseJobNumber as varchar) + '.';
RAISERROR(@msg, 16, 1) -- will cause branch to CATCH
END
/* Okay to generate bill */
/* If not in a transaction, begin one */
IF @@TRANCOUNT = 0
BEGIN
BEGIN TRANSACTION
SET @transactionBegun = 1;
END
EXECUTE @RC = [Billing].[_1_GeneratePsPreBillData]
@PsBaseJobNumber
,@pUser
,@pResultCode OUTPUT;
IF @pResultCode = 0
BEGIN
-- stored proced ran to successful completion
EXECUTE @RC = [Billing].[_2_GetBillingDataForXmlGeneration]
@pUseProductionPsSystem
,@PsBaseJobNumber
,@pResultCode OUTPUT;
IF @pResultCode = 0
BEGIN
-- stored proced ran to successful completion
IF @transactionBegun = 1
-- all table data has been created/updated
COMMIT TRANSACTION
-- Output XML bill to file
EXECUTE @RC = [Billing].[_3_GenerateBillingXmlFilesForPsJob]
@PsBaseJobNumber
,@pFilePath
,@pExportDateTime
,@pResultCode OUTPUT;
IF @pResultCode <> 0
BEGIN
-- called stored procedure failed
SET @msg = '[Billing].[_3_GenerateBillingXmlFilesForPsJob]() failed for job ' + CAST(@PsBaseJobNumber as varchar);
RAISERROR(@msg, 16, 1) -- will cause branch to CATCH
END
END
ELSE
BEGIN
-- called stored procedure failed
SET @msg = '[Billing].[_2_GetBillingDataForXmlGeneration]() failed for job ' + CAST(@PsBaseJobNumber as varchar);
RAISERROR(@msg, 16, 1) -- will cause branch to CATCH
END
END
ELSE
BEGIN
-- called stored procedure failed
SET @msg = '[Billing].[_1_GeneratePsPreBillData]() failed for job ' + CAST(@PsBaseJobNumber as varchar);
RAISERROR(@msg, 16, 1) -- will cause branch to CATCH
END
-- bill generation was successful
IF @billGenerationId IS NOT NULL
UPDATE [Billing].[BillGeneration]
SET [Status] = 'Successful', [ProcessEndDateTimeUtc] = GETUTCDATE()
WHERE [Id] = @billGenerationId;
END TRY
BEGIN CATCH
-- rollback transaction if we started one
IF @transactionBegun = 1
ROLLBACK TRANSACTION
-- record the error
INSERT [Billing].[BillGenerationError] (
[DateTime], [Object], [ErrorNumber], [ErrorMessage]
) VALUES (
GETDATE(), OBJECT_NAME(@@PROCID), ERROR_NUMBER(), ERROR_MESSAGE()
);
-- bill generation failed
IF @billGenerationId IS NOT NULL
UPDATE [Billing].[BillGeneration]
SET [Status] = 'Failed'
,[Note] = ERROR_MESSAGE()
,[ProcessEndDateTimeUtc] = GETUTCDATE()
WHERE [Id] = @billGenerationId;
SELECT ERROR_NUMBER() as ErrorNumber;
SELECT ERROR_MESSAGE() as ErrorMessage;
SET @pResultCode = 1
END CATCH
END
正如@Lukasz Szozda 在他对我的问题的评论之一中暗示的那样,问题是当 SQL 代理作业执行 BCP.EXE
命令时,它 运行ning 在SQL 代理使用的服务帐户,对我来说是相当严格的 "Local System" 帐户。在这一点上,我很明显必须使用代理帐户。所以我在 Operating System (CmdExec)
下创建了一个代理,这是唯一有意义的选择。
我回到作业步骤将其更改为使用 Proxy,但随后注意到在其当前类型 Transact-SQL script (TSQL)
中,无法分配 Proxy 帐户。
尝试了几次之后,最终决定将作业步骤中的 TSQL 语句放入新的存储过程中,然后从 SQL 命令中调用该存储过程-行可执行文件 SQLCMD.EXE
。然后,我将作业步骤类型从 Transact-SQL script (TSQL)
更改为 Operating System (CmdExec)
。然后我可以将 Run As
字段设置为我之前创建的代理。我将 运行 的命令指定为 CMD.EXE /c SQLCMD.EXE -S [ServerName] -Q "EXEC [NewProcedureName] [parameters]"
.
如果您对我 运行ning SQLCMD.EXE
under CMD.EXE
感到好奇,那是因为新存储过程的参数之一是当前日期一种特定格式 ('%date:~4,10%'
),SQL 服务器作业执行环境不支持,但 CMD.EXE
肯定支持。
总的来说,我认为这比我预期的要付出更多的努力。
我有一个存储过程,它在 SQL Server Management Studio 中运行良好,没有任何错误。但是,当相同的存储过程作为 SQL 代理作业的一个步骤执行时,它终止于:
Error 3930: The current transaction cannot be committed and cannot support operations that write to the log file. Roll back the transaction.
存储过程位于名为 [Billing]
的架构中。它使用的大多数表也在 [Billing]
架构中。
主存储过程开始一个数据库事务。主存储过程调用的存储过程在继承的事务上完成它们的所有工作。主存储过程负责提交或回滚事务。
数据库用户 运行 SQL 代理作业步骤不在系统管理员角色中,也不是 dbo。它属于 db_datareader 和 db_datawriter 数据库角色,并在 [Billing]
架构中被授予删除、执行、插入、引用、Select 和更新权限。
这是主要的存储过程:
CREATE PROCEDURE [Billing].[GenerateBillXml]
@pUseProductionPsSystem bit
,@pPeriodYear int
,@pPeriodMonthNumber int
,@pDocumentTypeName varchar(20)
,@pUser varchar(100)
,@pFilePath varchar(500)
,@pExportDateTime datetime2(7)
,@pResultCode int = 0 OUTPUT
AS
BEGIN
SET NOCOUNT ON;
SET @pResultCode = 0;
DECLARE @transactionBegun bit = 0
,@RC int = 0
,@processDateTimeUtc datetime2(7) = GETUTCDATE()
,@billGenerationId int
,@PsJobSuffix char(2)
,@periodDate date
,@periodDateString char(6)
,@PsBaseJobNumber char(6)
,@okayToRun int = 0
,@msg varchar(500);
BEGIN TRY
/* calculate the period date */
SET @periodDate = CONVERT(date,CAST(@pPeriodMonthNumber as varchar) + '/01/' + CAST(@pPeriodYear as varchar))
SET @periodDateString = CONVERT(varchar, @periodDate, 12); -- yyMMdd
/* retrieve the job suffix */
SELECT @PsJobSuffix = CAST([PsJobSuffix] as char(2))
FROM [dbo].[DocumentType] dt
WHERE [Name] like @pDocumentTypeName;
/* build the base job number */
SET @PsBaseJobNumber = LEFT(@periodDateString, 4) + @PsJobSuffix
/*
* We've made it past the input check - record the fact that we're generating a bill
*/
INSERT [Billing].[BillGeneration] (
[PsBaseJobNumber], [Status], [RunBy], [ProcessDateTimeUtc]
) VALUES (
@PsBaseJobNumber, 'Running', @pUser, @processDateTimeUtc
);
IF @@ROWCOUNT = 1
SET @billGenerationId = SCOPE_IDENTITY();
EXECUTE @RC = [Billing].[_0_OkayToGenerateBill]
@PsBaseJobNumber
,@okayToRun OUTPUT
,@pResultCode OUTPUT;
IF @pResultCode = 0
BEGIN
-- called stored procedure completed without error
IF @okayToRun = -1 -- this bill has already been generated
BEGIN
SET @msg = 'The billing for job ' + CAST(@PsBaseJobNumber as varchar) + ' has already been produced.';
RAISERROR(@msg, 16, 1)
END
IF @okayToRun = -2 -- too early to run billing for this period
BEGIN
SET @msg = 'It is too early to generate billing for job ' + CAST(@PsBaseJobNumber as varchar) + '.';
RAISERROR(@msg, 16, 1)
END
IF @okayToRun <> 1 -- unknown error...
BEGIN
SET @msg = 'Unknown error occured while determining whether okay to generate bill for job ' + CAST(@PsBaseJobNumber as varchar) + '.';
RAISERROR(@msg, 16, 1)
END
END
ELSE
BEGIN
SET @msg = 'Unknown failure in sub-stored procedure [Billing].[_0_OkayToRun]() for job ' + CAST(@PsBaseJobNumber as varchar) + '.';
RAISERROR(@msg, 16, 1) -- will cause branch to CATCH
END
/* Okay to generate bill */
/* If not in a transaction, begin one */
IF @@TRANCOUNT = 0
BEGIN
BEGIN TRANSACTION
SET @transactionBegun = 1;
END
EXECUTE @RC = [Billing].[_1_GeneratePsPreBillData]
@PsBaseJobNumber
,@pUser
,@pResultCode OUTPUT;
IF @pResultCode = 0
BEGIN
-- stored proced ran to successful completion
EXECUTE @RC = [Billing].[_2_GetBillingDataForXmlGeneration]
@pUseProductionPsSystem
,@PsBaseJobNumber
,@pResultCode OUTPUT;
IF @pResultCode = 0
BEGIN
-- stored proced ran to successful completion
IF @transactionBegun = 1
-- all table data has been created/updated
COMMIT TRANSACTION
-- Output XML bill to file
EXECUTE @RC = [Billing].[_3_GenerateBillingXmlFilesForPsJob]
@PsBaseJobNumber
,@pFilePath
,@pExportDateTime
,@pResultCode OUTPUT;
IF @pResultCode <> 0
BEGIN
-- called stored procedure failed
SET @msg = '[Billing].[_3_GenerateBillingXmlFilesForPsJob]() failed for job ' + CAST(@PsBaseJobNumber as varchar);
RAISERROR(@msg, 16, 1) -- will cause branch to CATCH
END
END
ELSE
BEGIN
-- called stored procedure failed
SET @msg = '[Billing].[_2_GetBillingDataForXmlGeneration]() failed for job ' + CAST(@PsBaseJobNumber as varchar);
RAISERROR(@msg, 16, 1) -- will cause branch to CATCH
END
END
ELSE
BEGIN
-- called stored procedure failed
SET @msg = '[Billing].[_1_GeneratePsPreBillData]() failed for job ' + CAST(@PsBaseJobNumber as varchar);
RAISERROR(@msg, 16, 1) -- will cause branch to CATCH
END
-- bill generation was successful
IF @billGenerationId IS NOT NULL
UPDATE [Billing].[BillGeneration]
SET [Status] = 'Successful', [ProcessEndDateTimeUtc] = GETUTCDATE()
WHERE [Id] = @billGenerationId;
END TRY
BEGIN CATCH
-- rollback transaction if we started one
IF @transactionBegun = 1
ROLLBACK TRANSACTION
-- record the error
INSERT [Billing].[BillGenerationError] (
[DateTime], [Object], [ErrorNumber], [ErrorMessage]
) VALUES (
GETDATE(), OBJECT_NAME(@@PROCID), ERROR_NUMBER(), ERROR_MESSAGE()
);
-- bill generation failed
IF @billGenerationId IS NOT NULL
UPDATE [Billing].[BillGeneration]
SET [Status] = 'Failed'
,[Note] = ERROR_MESSAGE()
,[ProcessEndDateTimeUtc] = GETUTCDATE()
WHERE [Id] = @billGenerationId;
SELECT ERROR_NUMBER() as ErrorNumber;
SELECT ERROR_MESSAGE() as ErrorMessage;
SET @pResultCode = 1
END CATCH
END
正如@Lukasz Szozda 在他对我的问题的评论之一中暗示的那样,问题是当 SQL 代理作业执行 BCP.EXE
命令时,它 运行ning 在SQL 代理使用的服务帐户,对我来说是相当严格的 "Local System" 帐户。在这一点上,我很明显必须使用代理帐户。所以我在 Operating System (CmdExec)
下创建了一个代理,这是唯一有意义的选择。
我回到作业步骤将其更改为使用 Proxy,但随后注意到在其当前类型 Transact-SQL script (TSQL)
中,无法分配 Proxy 帐户。
尝试了几次之后,最终决定将作业步骤中的 TSQL 语句放入新的存储过程中,然后从 SQL 命令中调用该存储过程-行可执行文件 SQLCMD.EXE
。然后,我将作业步骤类型从 Transact-SQL script (TSQL)
更改为 Operating System (CmdExec)
。然后我可以将 Run As
字段设置为我之前创建的代理。我将 运行 的命令指定为 CMD.EXE /c SQLCMD.EXE -S [ServerName] -Q "EXEC [NewProcedureName] [parameters]"
.
如果您对我 运行ning SQLCMD.EXE
under CMD.EXE
感到好奇,那是因为新存储过程的参数之一是当前日期一种特定格式 ('%date:~4,10%'
),SQL 服务器作业执行环境不支持,但 CMD.EXE
肯定支持。
总的来说,我认为这比我预期的要付出更多的努力。