Как установить таймаут выполнения запроса в Delphi с использованием TFDQuery и ResourceOptions.CmdExecTimeout
При работе с базами данных в Delphi через FireDAC разработчики часто сталкиваются с необходимостью ограничивать время выполнения SQL-запросов. В этой статье мы рассмотрим, как правильно использовать свойство ResourceOptions.CmdExecTimeout компонента TFDQuery для установки таймаута выполнения запросов, а также разберем альтернативные подходы к решению этой задачи.
Проблема с таймаутом выполнения запросов
Как показывает практика (и пример из контекста), свойство CmdExecTimeout не всегда работает так, как ожидается, особенно с определенными типами SQL-запросов. Рассмотрим пример кода, который демонстрирует проблему:
procedure TfrmMainForm.btnSynchronousOpenClick(Sender: TObject);
begin
var Query := TFDQuery.Create(Self);
try
Query.Connection := cnConnection;
Query.SQL.Text := '''
DECLARE @X int
WHILE 1=1 -- Infinite Loop
SET @X = 1
''';
Query.ResourceOptions.CmdExecTimeout := 1000; // 1 секунда
try
Query.Open;
ShowMessage('Query Opened');
except
on E: EFDDBEngineException do
if E.Kind = ekCmdAborted then
ShowMessage('Query Aborted');
end;
finally
Query.Free;
end;
end;
В этом примере запрос содержит бесконечный цикл, и ожидается, что через 1 секунду будет вызвано исключение EFDDBEngineException с Kind = ekCmdAborted. Однако на практике таймаут не срабатывает, и запрос продолжает выполняться.
Почему таймаут не работает в некоторых случаях?
Как выяснилось в ходе исследования, проблема связана с тем, как FireDAC и SQL Server взаимодействуют друг с другом:
FireDAC передает параметр таймаута драйверу SQL Server
Драйвер SQL Server применяет этот таймаут только к определенным типам запросов (SELECT, UPDATE, DELETE)
Для "бесконечных" запросов, которые не возвращают данные (как в примере), таймаут не применяется
Решение 1: Использование реальных SELECT-запросов
Простейшее решение - убедиться, что ваш запрос является реальным SELECT-запросом. В этом случае таймаут будет работать как ожидается:
procedure TfrmMainForm.btnWorkingTimeoutClick(Sender: TObject);
begin
var Query := TFDQuery.Create(Self);
try
Query.Connection := cnConnection;
Query.SQL.Text := 'SELECT * FROM LargeTable WHERE ComplexCondition = 1';
Query.ResourceOptions.CmdExecTimeout := 5000; // 5 секунд
try
Query.Open;
ShowMessage('Query успешно выполнен');
except
on E: EFDDBEngineException do
if E.Kind = ekCmdAborted then
ShowMessage('Запрос прерван по таймауту');
end;
finally
Query.Free;
end;
end;
Решение 2: Асинхронное выполнение с ручной проверкой таймаута
Для случаев, когда необходимо ограничить время выполнения любых запросов (включая "бесконечные" циклы), можно использовать асинхронное выполнение с ручной проверкой таймаута:
procedure TfrmMainForm.btnAsyncWithManualTimeoutClick(Sender: TObject);
var
Query: TFDQuery;
StartTime: Cardinal;
Timeout: Integer;
begin
Query := TFDQuery.Create(Self);
try
Query.Connection := cnConnection;
Query.SQL.Text := '''
DECLARE @X int
WHILE 1=1 -- Infinite Loop
SET @X = 1
''';
Query.ResourceOptions.CmdExecMode := amAsync;
Query.AfterOpen := QueryOpened;
StartTime := GetTickCount;
Timeout := 3000; // 3 секунды
Query.Open;
// Проверяем таймаут в цикле
while not (Query.Active or Query.Eof) do
begin
if GetTickCount - StartTime > Timeout then
begin
Query.Abort;
raise Exception.Create('Запрос прерван по таймауту');
end;
Application.ProcessMessages;
Sleep(100);
end;
finally
Query.Free;
end;
end;
Решение 3: Использование SQL Server Command Timeout
Для SQL Server можно установить таймаут на уровне соединения:
procedure TfrmMainForm.SetConnectionTimeout;
begin
// Установка таймаута соединения (в секундах)
cnConnection.ExecSQL('SET LOCK_TIMEOUT 5'); // 5 секунд
cnConnection.ExecSQL('SET REMOTE_PROC_TRANSACTIONS OFF');
end;
Решение 4: Комбинированный подход
Для максимальной надежности можно комбинировать несколько подходов:
procedure TfrmMainForm.btnCombinedApproachClick(Sender: TObject);
var
Query: TFDQuery;
Thread: TThread;
Completed: Boolean;
ErrorMsg: string;
begin
Query := TFDQuery.Create(nil);
try
Query.Connection := cnConnection;
Query.SQL.Text := 'Ваш SQL-запрос здесь';
Query.ResourceOptions.CmdExecTimeout := 5000; // 5 секунд
Completed := False;
ErrorMsg := '';
// Запускаем запрос в отдельном потоке
Thread := TThread.CreateAnonymousThread(
procedure
begin
try
Query.Open;
TThread.Synchronize(nil,
procedure
begin
Completed := True;
end);
except
on E: Exception do
begin
ErrorMsg := E.Message;
TThread.Synchronize(nil,
procedure
begin
Completed := True;
end);
end;
end;
end);
Thread.Start;
// Ждем завершения с таймаутом
var StartTime := GetTickCount;
while not Completed and (GetTickCount - StartTime < 10000) do // 10 секунд
begin
Sleep(100);
Application.ProcessMessages;
end;
if not Completed then
begin
Query.Abort;
Thread.Terminate;
raise Exception.Create('Запрос прерван по таймауту');
end;
if ErrorMsg <> '' then
raise Exception.Create(ErrorMsg);
// Работаем с результатами запроса
ShowMessage('Запрос успешно выполнен. Записей: ' + Query.RecordCount.ToString);
finally
Query.Free;
end;
end;
Альтернативные решения
Если стандартные методы не работают для вашего случая, рассмотрите следующие альтернативы:
Использование хранимых процедур с таймаутом: CREATE PROCEDURE dbo.ExecuteWithTimeout
@TimeoutSeconds INT,
@SQL NVARCHAR(MAX) AS BEGIN
SET LOCK_TIMEOUT @TimeoutSeconds * 1000;
EXEC sp_executesql
@SQL;
END
Ограничение времени выполнения на стороне сервера с помощью Resource Governor в SQL Server.
Использование TFDEventAlerter для мониторинга длительных операций.
Заключение
Хотя свойство ResourceOptions.CmdExecTimeout в FireDAC является основным способом установки таймаута выполнения запросов, оно работает не во всех случаях. Для надежного ограничения времени выполнения запросов рекомендуется:
По возможности использовать реальные SELECT-запросы
Комбинировать CmdExecTimeout с асинхронным выполнением и ручной проверкой таймаута
Устанавливать таймауты на уровне соединения с базой данных
Для сложных случаев использовать многопоточные подходы
Приведенные в статье решения помогут вам создать более надежные приложения, которые не будут "зависать" при выполнении длительных SQL-запросов.
Статья объясняет, как установить таймаут выполнения SQL-запросов в Delphi с помощью TFDQuery и ResourceOptions.CmdExecTimeout, а также рассматривает альтернативные решения для случаев, когда стандартный подход не работает.
Комментарии и вопросы
Получайте свежие новости и обновления по Object Pascal, Delphi и Lazarus прямо в свой смартфон. Подпишитесь на наш Telegram-канал delphi_kansoftware и будьте в курсе последних тенденций в разработке под Linux, Windows, Android и iOS
Материалы статей собраны из открытых источников, владелец сайта не претендует на авторство. Там где авторство установить не удалось, материал подаётся без имени автора. В случае если Вы считаете, что Ваши права нарушены, пожалуйста, свяжитесь с владельцем сайта.