【问题标题】:ADODataSet.Open bypasses try catch with `ArgumentOutOfRange` exception, hangs application - Delphi 10.2ADODataSet.Open 绕过 try catch 与 `ArgumentOutOfRange` 异常,挂起应用程序 - Delphi 10.2
【发布时间】:2020-05-08 19:02:26
【问题描述】:

我维护一个在服务器环境中作为服务运行的应用程序。它是多线程的,每个线程都根据任务队列工作。此任务队列只是一个以“作业类型”为值的字符串列表。因此,虽然可能有多个线程正在运行,但每个线程将是不同的作业,并且每个线程在内部一次只运行一个任务。

TADODataSet 上调用Open 时,我遇到了间歇性问题。有时,并非总是如此,并且没有明显的模式,Data.Win.ADODB 会抛出一个EArgumentOutOfRangeException,绕过我自己捕获任何异常的尝试。此异常会挂起整个线程并阻止将来执行,直到我完全重新启动服务。

作为 Delphi 世界的新手,我已经在这个问题上摸索了很长一段时间,并且一直在努力寻找任何答案。我的问题是:为什么会发生这种情况,我该如何阻止、捕捉或修复它?

这是我的违规代码的 sn-p。这是堆栈跟踪的来源方法。它是从同一单元中的另一个方法调用的,在这里我打开一个不同的数据集,循环遍历其记录,并在每条记录上调用此函数以根据传入的值获取一些信息。

function TFQFoo.DoSomething(IncNo : Int64): string;
var
  ItemList : string;
  MySQL: string;
  ComponentString: string;
begin
  result:='';
  if IncNo<=0 then
    Exit;
  ItemList := '';
  MyQuery.Close;
  MySQL := 'select ID from tbl ' +
    ' where val = ' + IntToStr(IncNo) +
    ' order by col1 DESC, col2, col3';

  try
    try
      MyQuery.CommandText := (MySQL);
      MyQuery.Open;

      while not (MyQuery.EOF) do
      begin
        if (ItemList <> '') then
          ItemList := ItemList + ',';
        ItemList := ItemList +
          MyQuery.FieldbyName('ID').asstring;
        MyQuery.Next;
      end;
    except
      // exception handling code omitted for brevity -- none of it
      // is ever reached, anyway (see below)
    end;
  finally
    MyQuery.Close;
  end;
  Result := ItemList;
end;

异常的调用堆栈表明它发生在Open。没有try..catch 块将捕获异常并为我记录——我必须使用 EurekaLog 才能查看任何详细信息。堆栈跟踪方法(太大,无法在此处发布)如下所示:

  1. TFQFoo.DoSomething
  2. TDataSet.Open
  3. ...内部的东西
  4. TADOConnection.ExecuteComplete
  5. CheckForAsyncExecute
  6. ...
  7. TCustomConnection.GetDataSet
  8. TListHelper.GetItemRange

考虑到我的 TADODataSet 组件可能以某种方式损坏/它的属性在运行时发生了改变,我添加了一些日志记录来为我捕获该数据,以便我可以查看那里是否发生了一些奇怪的事情。我没有看到任何东西,但这是以防万一。

object MyQuery: TADODataSet
  AutoCalcFields = False
  CacheSize = 15
  Connection = FGlobals.RIMSDB
  CursorType = ctStatic
  LockType = ltReadOnly
  CommandText = 
    'select ID from tbl  where val = 202005070074 order by col1 ' +
    'DESC, col2, col3'
  ParamCheck = False
  Parameters = <>
  Left = 32
  Top = 216
end

出于好奇,这是实际抛出异常的方法,来自Data.Win.ADODB。注意异常,我猜它会跳过我自己的 try..catch 块并将异常直接发送到 EurekaLog。

procedure CheckForAsyncExecute;
var
  I: Integer;
begin
  try
    if not Assigned(pError) and Assigned(pRecordset) and
       ((pRecordset.State and adStateOpen) <> 0) then
      for I := 0 to DataSetCount - 1 do
        if (DataSets[I].Recordset = pRecordset) and (eoAsyncExecute in DataSets[I].ExecuteOptions) then
        begin
          DataSets[I].OpenCursorComplete;
          Break;
        end;
  except
    ApplicationHandleException(Self);
  end;
end;

我尝试过的:

  • 在 ADODataSet 本身上调整组件属性的许多次迭代
  • 在设计器中使用 CommandText 和参数,并在运行时执行之前分配参数
  • 向查询本身添加/删除 (NOLOCK) 提示
  • 将问题提交给我团队的高级成员以征求意见
  • 谷歌搜索(数小时)
  • 阅读 Delphi 和 ADO 文档(对此没有收获)
  • 尝试复制 - 我从来没有在我使用的任何测试系统上发生这种情况。这让我认为它可能与环境有关,但我完全不知道如何

我的问题,重申一下:

如何阻止这种情况发生?我究竟做错了什么?我不想只抓住EArgumentOutOfRangeException;我想首先了解它为什么会发生,并防止它在未来发生。

我知道有时,查询执行不会返回结果,但是由于低级代码绕过了我自己的 catch 语句,因此从未见过或遇到过典型的 CommandText does not return a result set 消息。除此之外,我不知道还要寻找什么。

到目前为止,我只发现了另一个类似的情况,但它仅与未捕获的异常有关:http://www.delphigroups.info/2/d9/410191.html

【问题讨论】:

  • 如果您还没有尝试过在CheckForAsyncExecute 中的ApplicationHandleException(Self) 上放置调试器断点?可能是异步线程中发生了异常,并且在您的处理程序看到它之前正在处理它。好 q,顺便说一句,+1
  • @MartynA 谢谢!我有,但不幸的是,我无法在我自己的测试中重现该问题。
  • 有什么有用的东西可以发送到TADOConnection.OnInfoMessage 事件吗?
  • @Brian 简短回答:不。我已将事件连接起来以在if EventStatus &lt;&gt; esOK 条件下记录消息——因此esOK 以外的任何状态都应记录整个消息;但是,没有任何记录。同样,OnExecuteComplete 也有类似的功能,但当然也没有任何记录。由于在 6 小时内运行的查询数量庞大,删除此检查有点问题。

标签: delphi ado delphi-10.2-tokyo


【解决方案1】:

CheckForAsyncExecute() 中对ApplicationHandleException(Self) 的调用正在吞噬异常,这就是您的except 块没有被触发的原因:

// in Data.Win.ADODB.pas:

procedure CheckForAsyncExecute;
var
  I: Integer;
begin
  try
    ...
  except
    ApplicationHandleException(Self); // <-- a caught exception is NOT re-raised here!
  end;
end;

Data.Win.ADODB单元内部,捕获到的异常会调用单元自己的ApplicationHandleException()函数,如果被赋值则调用System.Classes.ApplicationHandleException,否则直接退出:

// in Data.Win.ADODB.pas:

procedure ApplicationHandleException(Sender: TObject);
begin
  if Assigned(System.Classes.ApplicationHandleException) then
    System.Classes.ApplicationHandleException(Sender);
end;

System.Classes.ApplicationHandleException 被初始化为nil

在 VCL1 和 FMX 应用程序中,TApplication 构造函数将TApplication.HandleException() 方法分配给System.Classes.ApplicationHandleException,其中HandleException() 忽略EAbort 异常,并调用@987654336 @ 事件处理程序(如果已分配)、TApplication.ShowException() 方法或 System.SyUtils.ShowException() 函数,具体取决于正在处理的异常类型。

    1:在 VCL TService 应用程序中,TServiceApplication 在内部使用 Vcl.Forms.TApplicationTApplication.ShowException() 在弹出的 MessageBox 中向用户显示异常的详细信息,然后退出,然后System.SysUtils.ShowException() 在 Console 或 MessageBox 中向用户显示异常的详细信息,然后退出。

因此,ADO 的CheckForAsyncExecute() 绝不会将捕获的异常重新引发到用户代码中。不用说,在服务中显示弹出消息框不是一个好主意,因为用户可能看不到它,因此他们可以将其关闭。

当然,最好的选择是首先避免引发EArgumentOutOfRangeException 异常。但还有其他条件也可能引发异常。

因此,您自己处理吞噬的 ADO 异常并避免弹出 MessageBoxes 的唯一选择是分配一个 TApplication.OnException 事件处理程序(直接或通过 TApplicationEvents 组件)。

【讨论】:

  • 感谢您的回复。我已经明白为什么我的异常没有被我自己的代码捕获;我的问题更具体地说是为什么首先抛出异常。 ADO 如何在这样的检查中超出范围:for I := 0 to DataSetCount - 1 do(在CheckForAsyncExecute)?是我的查询还是我的代码是罪魁祸首,还是这是其他地方的错误?
  • 我还想说明这是作为服务运行的;因此,在执行我的代码时不会损害任何消息框;)当我在问题中提到“日志记录”时,我的意思是我有一些实用程序可以在运行服务的生命周期内将消息写入文本文件
  • 好吧,既然您已经在跟踪 ADO 源代码,那么您应该能够找到 EArgumentOutOfRangeException 实际上是从哪里产生的,以及在什么条件下产生的。至于日志记录,您可能正在将自己的日志消息写入文件,但 RTL 不会这样做。正如您在我的回答中看到的那样,EArgumentOutOfRangeException 可能会导致显示 MessageBox,这可以解释为什么调用线程会挂起,直到服务重新启动。
  • 啊,我误解了你所说的 MessageBoxes 的意思,我很抱歉。对于这项服务,我使用 EurekaLog 来拦截我的应用程序异常,并将 Dialogs -> Dialog Type 设置为“None”。这会涵盖那个问题吗?至于确定问题发生的条件,这就是我的不足之处——我能想到的一切都没有发现任何新的或有用的信息,而且我无法重现该问题以便自己调试。
  • @BrandonGandy 这首先取决于 EurekaLog 如何捕获异常,是否将自己的处理程序分配给TApplication.OnException,甚至直接分配给System.Classes.ApplicationHandleException,或者通过其他一些方法捕获异常机制。我不知道 EurekaLog 是如何实现的细节。至于跟踪,你有源代码。在EArgumentOutOfRangeException 构造函数中放置一个断点,并在引发异常时查看调用堆栈。如果您自己无法重现它,您希望如何修复它?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-08-10
  • 2011-09-16
  • 1970-01-01
  • 2019-10-24
  • 2011-04-01
  • 1970-01-01
  • 2013-03-09
相关资源
最近更新 更多