4

I maintain an application that runs as a service in a server environment. It is multithreaded, where each thread does work according to a task queue. This task queue is just a list of strings with "job types" as their values. So while several threads may be running, each thread would be a different job, and each thread internally runs just one task at a time.

I'm experiencing an intermittent issue that happens when calling Open on a TADODataSet. Sometimes, not always, and with no discernible pattern, Data.Win.ADODB will throw an EArgumentOutOfRangeException, bypassing my own attempt to catch any exceptions. This exception hangs the entire thread and prevents future execution from being possible until I completely restart the service.

Being relatively new to the world of Delphi, I've been scratching my head at this issue for quite some time, and have struggled to find any answers. My question is: why is this happening, and how do I stop, catch, or fix it?

Here is a snippet of my offending code. This is the method from which the stack trace originates. It is called from another method in the same unit, where I open a different dataset, loop through its records, and on each record call this function to get some info based on the value passed in.

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;

The call stack from the exception indicates that it's occurring at Open. No try..catch block will capture the exception and log it for me -- I have to use EurekaLog in order to see any details. The stack trace methods (too big to post here) look like:

  1. TFQFoo.DoSomething
  2. TDataSet.Open
  3. ... internal things
  4. TADOConnection.ExecuteComplete
  5. CheckForAsyncExecute
  6. ...
  7. TCustomConnection.GetDataSet
  8. TListHelper.GetItemRange

Thinking possibly my TADODataSet component was somehow getting corrupted / its properties altered at runtime, I added some logging to capture that data for me so I could see if something funky was going on there. I didn't see anything, but here it is in case it's pertinent.

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

For the curious, this is the method actually throwing the exception, from Data.Win.ADODB. Note the except, which I guess hops over my own try..catch block and sends the exception straight to 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;

What I have tried:

  • Many, many iterations of tweaking component properties on the ADODataSet itself
  • Using the CommandText and Parameters from within the designer, and assigning the parameter prior to execution at runtime
  • Adding / removing a (NOLOCK) hint to the query itself
  • Taken the problem to senior members of my team for input
  • Googling (for hours)
  • Reading up on Delphi and ADO documentation (not fruitful for this)
  • Attempted reproduction - I've never been able to get this to occur on any test system I use. This leads me to think it may be environment-related, but I have absolutely no clue how

My question, restated:

How do I stop this from happening? What am I doing wrong? I don't want to just catch the EArgumentOutOfRangeException; I want to learn why it's happening in the first place and prevent it from happening in the future.

I know that sometimes, the query execution will not return results, but the typical CommandText does not return a result set message is never seen nor encountered due to the lower-level code bypassing my own catch statement. Beyond this, I don't know what else to look for.

I've only found one other occurrence of something similar so far, but it relates just to the exception not getting caught: http://www.delphigroups.info/2/d9/410191.html

Brandon Gandy
  • 41
  • 1
  • 3
  • If you haven't already, have you tried putting a debugger breakpoint on `ApplicationHandleException(Self)` in `CheckForAsyncExecute`? It may be that the exception is occuring in the async thread and is being handled before your handler would see it. Good q, btw, +1 – MartynA May 08 '20 at 19:46
  • @MartynA Thank you! I have, but unfortunately have never been able to reproduce the issue in my own testing. – Brandon Gandy May 08 '20 at 19:49
  • Anything useful get sent to the `TADOConnection.OnInfoMessage` event? – Brian May 08 '20 at 20:54
  • @Brian Short answer: nope. I have the event wired up to log messages on the condition `if EventStatus <> esOK` -- so any status other than `esOK` should log the entire message; but, nothing gets logged. Similarly, `OnExecuteComplete` has a similar bit of functionality, but of course nothing gets logged there, either. Removing this check is a little problematic due to the sheer number of queries that get run over a 6-hour period. – Brandon Gandy May 08 '20 at 21:08

1 Answers1

2

The call to ApplicationHandleException(Self) in CheckForAsyncExecute() is swallowing exceptions, which is why your except block is not being triggered:

// 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;

Inside of the Data.Win.ADODB unit, caught exceptions will call the unit's own ApplicationHandleException() function, which then calls System.Classes.ApplicationHandleException if assigned, otherwise it simply exits:

// in Data.Win.ADODB.pas:

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

System.Classes.ApplicationHandleException is initialized to nil.

In both a VCL1 and FMX app, the TApplication constructor assigns the TApplication.HandleException() method to System.Classes.ApplicationHandleException, where HandleException() ignores EAbort exceptions, and calls the TApplication.OnException event handler (if assigned), the TApplication.ShowException() method, or the System.SyUtils.ShowException() function, depending on the type of exception being handled.

    1: in a VCL TService app, TServiceApplication uses Vcl.Forms.TApplication internally. TApplication.ShowException() displays the details of the exception to the user in a popup MessageBox and then exits, and System.SysUtils.ShowException() displays the details of the exception to the user in a Console or MessageBox and then exits.

So, at no point does ADO's CheckForAsyncExecute() re-raise a caught exception into user code. And needless to say, displaying a popup MessageBox in a service is not a good idea, as the user will likely not see it so they can dismiss it.

Of course, the best option would be to avoid the EArgumentOutOfRangeException exception from being raised in the first place. But there are other conditions that can also raise exceptions, too.

So, your only option to handle swallowed ADO exceptions yourself, and avoid popup MessageBoxes, is to assign a TApplication.OnException event handler (either directly, or via the TApplicationEvents component).

Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • Thank you for the response. I already understand why my exceptions are not getting caught by my own code; my question is more specifically *why* the exception is getting thrown in the first place. How does ADO go out-of-range in a check such as this: `for I := 0 to DataSetCount - 1 do` (in `CheckForAsyncExecute`)? Is my query or my code the culprit, or is this a bug elsewhere? – Brandon Gandy May 08 '20 at 21:55
  • I also want to make clear this is running as a service; as such, no MessageBoxes are harmed in the execution of my code ;) When I refer to 'logging' in my question I mean I have utilities that write messages to text files over the lifetime of the running service – Brandon Gandy May 08 '20 at 21:56
  • Well, since you are already tracing the ADO source code, then you should be able to find where the `EArgumentOutOfRangeException` is actually being raised from, and under what conditions. As for the logging, you may be writing your own log messages to files, but the RTL doesn't do that. As you can see in my answer, the `EArgumentOutOfRangeException` is likely leading to a MessageBox being displayed, which would explain why the calling thread hangs until the service is restarted. – Remy Lebeau May 08 '20 at 22:00
  • Ah, I misunderstood what you meant about MessageBoxes, my apologies. For this service I use EurekaLog to intercept my application exceptions, with the Dialogs -> Dialog Type set to "None". Would that cover that issue? As for determining the conditions under which the problem occurs, that's where I'm coming up short -- everything I can think to trace reveals nothing new or useful, and I'm unable to reproduce the issue in order to debug it myself. – Brandon Gandy May 08 '20 at 22:17
  • @BrandonGandy it depends on how EurekaLog is catching the exception in the first place, whether it assigns its own handler to `TApplication.OnException`, or even to `System.Classes.ApplicationHandleException` directly, or is catching exceptions through some other mechanism. I don't know the details about how EurekaLog is implemented. As for tracking, you have the source code. Put a breakpoint in the `EArgumentOutOfRangeException` constructor and look at the call stack when the exception is being raised. If you can't reproduce it yourself, how do you expect to fix it? – Remy Lebeau May 08 '20 at 22:24
  • The break point would never be reached because I cannot reproduce the issue in order to encounter the problem in my own system. BUT -- I think you've pointed me in the right direction. I do believe it may be related to EurekaLog and the way it's handling the exception in a multi-threaded environment. – Brandon Gandy May 08 '20 at 22:28
  • I've reviewed EurekaLog's documentation and my settings and confirmed a) it's correctly configured for a "non-visual" app (ie running as a service) so that no message boxes show on exceptions, and b) that EurekaLog is my application exception handler. I already knew B was true, since I'm getting a stack trace specifically from the ADO exception in the .el report; but needed to confirm A. So I'm back to trying to figure out why the exception is thrown in the first place. Thanks for your input on this. – Brandon Gandy May 11 '20 at 20:46