【发布时间】:2020-05-28 08:11:31
【问题描述】:
我想构建一个 Azure Devops 构建任务,该任务针对 SQL Server 2017 数据库执行一系列 SQL 脚本。我按照本教程构建任务:https://docs.microsoft.com/en-us/azure/devops/extend/develop/add-build-task?view=azure-devops
该任务通常会成功运行,并且我已经针对我的本地数据库(在 SQL Server 2017 Express 中)执行了各种脚本。我正在使用 npm 包“mssql/msnodesqlv8”(mssql 的本机 SQL Server 驱动程序)直接在 node.js 中连接和执行脚本。
async function executeBatches(script: string, pool: sql.ConnectionPool) {
const batches = script.split("\r\nGO");
for (const batch of batches) {
console.log("Executing script:", batch);
await pool.batch(batch);
}
}
现在我发现一个脚本以一种非常奇怪的方式失败。有问题的脚本在事务中对 4 个表执行多次重命名,如下所示:
BEGIN TRANSACTION
EXEC sp_rename 'table' , 'newTable'
EXEC sp_rename 'newTable.column', 'newColumn', 'COLUMN'
(repeat for several columns)
EXEC sp_rename 'dbo.PK_table', 'PK_newTable'
(repeat for 3 more tables)
COMMIT
在 SQL Server Management Studio 中,脚本可以正确执行。但在 Devops 任务中,此脚本在大约 18 次 sp_rename 调用后中止。没有抛出错误,事务保持打开状态。客户端将继续运行(因为它没有错误),在执行更多查询后,SQL Server 会启动回滚,当然会回滚自执行此脚本以来的所有更改。
我切换了脚本中的语句并尝试注释掉一些行,但它总是在大约 18 次 sp_rename 调用后中止。当我删除足够多的行以便有 18 个或更少的 sp_rename 调用时,该任务可以完全运行脚本并提交更改(不管哪一行)。
当我删除事务时,它会执行所有重命名,直到那个神奇的数字,然后仍然中止脚本并让最后一条语句的隐式事务保持打开状态,因此它仍然会在更多查询后回滚所有更改。
我运行了 SQL Profiler,它显示 StmtStarting 进行重命名,然后 BatchCompleted 出现错误“2 - Abort”,但没有显示其他错误或原因说明批处理被中止的原因。
执行脚本时,system_health 会话显示 2 个错误:
错误 #1
tds_flags "DisconnectDueToReadError, NetworkErrorFoundInInputStream, NormalDisconnect" 和 tds_input_buffer_error 109 的连接错误。
错误 #2
error_code 5023 的安全错误(表示“组或资源未处于执行请求操作的正确状态。”)
在线搜索这些错误没有得到可用的结果,因为它们要么没有解决方案,要么与登录问题有关,我认为情况并非如此,因为我可以很好地执行其他脚本。
我还检查了编码,并且通过节点“fs”库正确读取了脚本。
如果我能找到导致此问题的原因的任何帮助或指示,我们将不胜感激。
编辑:我开始使用 msnodesqlv8 构建一个较小的示例。
import tl = require("azure-pipelines-task-lib/task");
import fs = require("fs");
import path = require("path");
import util = require("util");
import { SqlClient } from "msnodesqlv8";
// tslint:disable-next-line: no-var-requires
const sqlClient: SqlClient = require("msnodesqlv8");
const open = util.promisify(sqlClient.open);
const query = util.promisify(sqlClient.query);
async function run() {
try {
const scriptDirectory = tl.getInput("ScriptDirectory", true) ?? "";
const connectionString = "Driver={ODBC Driver 13 for SQL Server};Server={.\\SQLEXPRESS};Uid={sa};Pwd={start};Database={MyDatabase};Encrypt={yes};TrustServerCertificate={yes}";
const scriptsDir = fs.readdirSync(scriptDirectory);
const con = await open(connectionString);
const close = util.promisify(con.close);
try {
const conQuery = util.promisify(con.query);
for (const file of scriptsDir) {
console.log("Executing:", file);
const script = readFileWithoutBom(path.join(scriptDirectory, file));
console.log("Executing script:", script);
// await query(connectionString, { query_str: script, query_timeout: 120 });
await conQuery({ query_str: script, query_timeout: 120 });
const insert = `INSERT INTO AppliedDatabaseScript (ScriptFile, DateApplied) VALUES ('${file}', GETDATE())`
console.log("Executing script:", insert);
// await query(connectionString, { query_str: insert });
await conQuery({ query_str: insert });
}
} finally {
await close();
}
}
catch (err) {
console.error(err);
tl.setResult(tl.TaskResult.Failed, err.message);
}
}
// Strip BOM. SQL Server won't execute certain scripts with BOM.
function readFileWithoutBom(filePath: string) {
return fs.readFileSync(filePath, "utf-8").replace(/^\uFEFF/, "");
}
行为仍然相同。我尝试为每个查询使用公共连接和单独的连接。它将回滚同一连接中的所有内容并继续,就好像没有发生错误一样。我还摆弄了查询超时,但根本与错误无关。
【问题讨论】:
-
在您的数据库上执行 18 个 sp_renames 需要多长时间?大约90秒?听起来像一个 CommandTimeout 问题,尽管它为什么不(至少开始)立即回滚事务是一个谜。 msnodesqlv8 调用此
query_timeout并没有提及默认值,但在 .NET 和其他平台中通常约为 90 秒。 -
@AlwaysLearning 该脚本几乎立即执行(不到 1 秒)。它绝对不会超时。
-
根据您的描述,如果 sp_rename 调用少于 18 次,则该任务可以正常工作。这似乎与超时有关。 mssql/msnodesqlv8 包有两个时间限制:
conn_timeout :2和query_timeout : 2。这两个步骤似乎有 2 秒的限制。您可以检查这两个步骤是否已超时。这是关于the package 的文档。 -
很抱歉造成混淆,但我没有直接使用 msnodesqlv8,而是使用来自 mssql package 的包装器 mssql/msnodesqlv8。我还用直接链接更新了帖子。正如我之前所说,这个查询在不到一秒的时间内执行,我还有其他更复杂、更长的查询可以正常执行。我可以尝试只用 msnodesqlv8 重写它,但这需要一些时间。
标签: node.js sql-server typescript azure-devops sql-server-2017