Jump to content
Mark Williams

ISAPI DLL concurrent requests

Recommended Posts

I have written an ISAPI DLL (Delphi 10.3) for IIS to carry out various functions (download/upload of documents, query tables etc).

It was working fine until I started firing more than one request at a time then the DLL simply crashed and I had to restart WWW service.

The DLL is being used to request smallish files (20-30KB) over a local network. These are being requested one at a time. If I make these requests the same time as one or two Head requests that all goes Kaput.

I though it may be the TWebRequestHandler's maxConnections so I set that to 200. It made no difference.

After a bit of digging around I noticed that the IIS ApplicationPool that I created for the DLL has a "Maximum Worker Processes" property and that this was set to 1. I have set this to 20 and now the DLL seems to be able to handle more than one concurrent request.

However, I noticed from my DLL's log file that it was now creating a new instance of the DLL for every request. I had a dig around on the web and from the information I found I have come to the conclusion that Maximum Worker Processes is probably not what I need. It seems to be designed for long-running processes, which is not what my DLL is designed for.

 

If not Maximum Worker Processes and increasing MaxConnections doesn't work how do I get my DLL to deal with concurrent requests.

 

Share this post


Link to post

Would you like to share some code? From what I learned about isapi from making xxm, it's important to catch any and all exceptions in your code, and not have them pass through into IIS as it can't properly handle Delphi exceptions and indeed may terminate the worker process. Typically you have a try except inside of every callback handler, the except clauses set an appropriate return value, but never re-raises, perhaps writes to an extra log.

Share this post


Link to post

I think maybe I didn't explain myself very well. Apologies. Plus I have managed to improve things a little since my first post. I noticed that my DLL was including Madshi's exception units. I have removed these and there is now an improvement. Whereas the DLL appeared to be hanging on concurrent calls resulting in a timeout error, my client app now gets a ENetHTTPClientException "Server returned an invalid or unrecognised response". But whereas the DLL was freezing it now continues to run albeit rejecting any concurrent requests.

 

I don't know what code to post as I am not getting any exceptions for any of my event handlers (I do have the event wrapped in try except blocks). I add a log entry in the BeforeDispatch event to advise what method etc has been called and one in the AfterDispatch to advise when it is done. I log the http response codes issued by the handlers when they are done.

 

I have set up a test app to fire off rapid requests via threads to download 14 files at slightly staggered intervals (10ms) so that they overlap. Accordingly, there are probably 14 concurrent requests being made at the same time. Only 1 returns successfully all the others fire a ENetHTTPException.

 

The first request executes correctly. I log the BeforeDispatch and AfterDispatch events and these fire ok. I also log the Response returned by the dlls' download method and this logs correctly for the first call, which means it has completed okay.

 

For concurrent calls, the BeforeDispatch event is not called. Also, within each method I log the fact it has been called as the first line of the method's code. For concurrent calls, the DLL is not even entering the handler for the method being called. It does not trigger any exception even though wrapped in a try except block.

 

I think the problem is a threading problem within the DLL, but I don't understand why. As mentioned above, if within the ISS ApplicationPool for the DLL I increase the number of Maximum Working Processes above 1 then all is okay (presumably as long as I don't make concurrent requests that exceed the max working processes) save for the fact that each (concurrent?) request seems to run a new DLL instance rather than making use of DLL threading. Whilst this sort of resolves the problem, I am certain it is not the way to do this from all I have read on the web.

 

To get threading working I can't work out whether there is something I need to configure on my website (it is configured for limitless connections), the ApplicationPool (can't see anything) or the app (and I used the wizard to create it so I would have thought not..

 

Below is the code from the DLL dpr file:

library MWCWebServer;

uses
  Winapi.ActiveX,
  System.Win.ComObj,
  System.SysUtils,
  Web.WebBroker,
  Web.Win.ISAPIApp,
  Web.Win.ISAPIThreadPool,
  WebModule in 'WebModule.pas' {WebModule1: TWebModule};

{$R *.res}

exports
  GetExtensionVersion,
  HttpExtensionProc,
  TerminateExtension;


begin
  try
  CoInitFlags := COINIT_MULTITHREADED;
  Application.Initialize;
  Application.WebModuleClass := WebModuleClass;
  {Application.MaxConnections:=200;
  NumberOfThreads:=Application.MaxConnections;
  Application.CacheConnections:=true;}
  Application.Run;
end.

I have tried setting MaximumConnections, CacheConnections. NumberOfThreads and none of these seem to do anything. But I can't imagine for a moment that the default no of threads for a webmodule is 1. So I guess it has to be something else.

 

I have tried to monitor how many connections there are. But within the webModule I cannot figure out how to access the Application property!

 

 

Share this post


Link to post

Web

26 minutes ago, Attila Kovacs said:

show web.config and MCVE

Web.config:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <system.net>
        <mailSettings>
            <smtp from="">
                <network host="" />
            </smtp>
        </mailSettings>
    </system.net>
</configuration>

I'll have to work on a MCVE. But do you have any suggestionsat this point why multi-threading may not to be working?

Share this post


Link to post

Not really. Make sure WebGarden is not turned on, you don't do anything in webmodule OnCreate and OnDestroy, check the Event Viewer, try writing into a logfile to see what executes and what not...

Share this post


Link to post

WebGarden? Is this when you increase the Maximum Worker Processes of the Application Pool above 1? If so, it works when I do this, but I appreciate it is not the solution and I have reduced it again to 1.

 

Nothing in WebModule's OnCreate and onDestroy. BTW what event should I use to free objects etc?

 

Nothing in the Event Viewer.

 

I am writing everything to a log file. See my last but one post.

 

Also, can you advise me how I can monitor the ActiveConnections property from the WebModule? I can't figure out how to access the Application object.

 

BTW thanks. I'll get some test projects uploaded asap.

 

 

Share this post


Link to post
2 minutes ago, Mark Williams said:

WebGarden? Is this when you increase the Maximum Worker Processes of the Application Pool above 1? If so, it works when I do this, but I appreciate it is not the solution and I have reduced it again to 1.

I think there are explicit WebGarden = true / false options in IIS, but your web.config does not has it, maybe you could check other config files.

 

3 minutes ago, Mark Williams said:

BTW what event should I use to free objects etc?

 

Nothing.

Always set up everything from scratch, and at the end of your events there should nothing be kept.

Avoid globals as much you can or use thread safe techniques to access them.

 

18 minutes ago, Mark Williams said:

I can't figure out how to access the Application object

I don't know how could be this possible. Ok, I also never needed it, but your events are running in a different thread every time you do a req, I'm not even sure that these threads are aware of an existing Application instance.

 

 

 

Share this post


Link to post

Btw. the fact that your broker works in garden mode, where the dll will be loaded every time you have a new request (as I understand it), indicates that you are using globals in non thread-safe mode.

  • Like 1

Share this post


Link to post
On ‎9‎/‎3‎/‎2019 at 2:13 PM, Attila Kovacs said:

Btw. the fact that your broker works in garden mode, where the dll will be loaded every time you have a new request (as I understand it), indicates that you are using globals in non thread-safe mode.

Spot on! That was the problem. As for WebGarden, I have check and you create a WebGarden by increasing the ApplicationPool's Maximum Worker Processes above 1. I am now running as non-web garden.

 

I have done a lot of research based on your helpful points above and eventually the mist began to clear. I now have a working ISAPI DLL which appears to be thread safe. Unfortunately, I couldn't find any good examples of how to do what I was trying to do.

 

My DLL uses global variables stored in an inifile. I don't want to load the inifile each time a new thread is fired to avoid repeated loading of the ini file.. So I need to load the inifile once when the DLL is first loaded. Also, I need to have a threadsafe logging system and finally I use a MimeTable in the DLL and I don't want to load that for each request.

 

Below is my relevant code (with the detail removed) to show I have gone about the above. It is working as far as I can see, but I would appreciate any tips for improvement or re potential pitfalls. Hopefully, this will then help others who are new ISAPI dll. It took me many hours of head scratching to get this far.

 

The dpr

library WebServer;

uses
  Winapi.ActiveX,
  System.Win.ComObj,
  System.SysUtils,
  Web.WebBroker,
  Web.Win.ISAPIApp,
  Web.Win.ISAPIThreadPool,
  WinAPI.Isapi2,
  WinAPI.Windows,
  System.SyncObjs,
  WebModule in 'WebModule.pas' {WebModule1: TWebModule};

{$R *.res}


//Read online somewhere that it is best to initialize/Finalize your globals in the following function 
//rather than in Initialization/Finalization sections of the webmodule}
function GetExtensionVersion(var Ver: THSE_VERSION_INFO): BOOL; stdcall;
begin
  Result := Web.Win.ISAPIApp.GetExtensionVersion(Ver);
  CriticalSection := TCriticalSection.Create;
  StartServer;
end;

function TerminateVersion(var Ver: THSE_VERSION_INFO): BOOL; stdcall;
begin
  Result := Web.Win.ISAPIApp.GetExtensionVersion(Ver);
  CloseServer;
  CriticalSection.Free;
end;

exports
  GetExtensionVersion,
  HttpExtensionProc,
  TerminateExtension;


begin
  CoInitFlags := COINIT_MULTITHREADED;
  Application.Initialize;
  Application.WebModuleClass := WebModuleClass; 
  {Application.MaxConnections:=100;} //if you want to increase beyond the default 32 for example
  NumberOfThreads:=Application.MaxConnections; //see comments following code samples. This is necessary even if you do not want to change the MaxConnections
  Application.Run;
end.

Stripped down WebModule

unit WebModule;

interface

uses WinApi.Windows, System.SysUtils, System.Classes, Web.CGIHTTP, Web.HTTPApp, Web.ReqMulti, System.SyncObjs, 
 ThreadFileLog, IDGlobalProtocols {, etc...};

type TLogType = (leNone, leCriticalError, leMajorError, leMinorError, leMajorEvents, leDevelopment);

type
    TWebModule1 = class(TWebModule)
    FDQuery: TFDQuery;
    FDPhysPgDriverLink: TFDPhysPgDriverLink;
    FDConnection: TFDConnection;
    procedure WebModule1HeadAction(Sender: TObject; Request: TWebRequest;
      Response: TWebResponse; var Handled: Boolean);
    procedure WebModuleBeforeDispatch(Sender: TObject; Request: TWebRequest;
      Response: TWebResponse; var Handled: Boolean);
    procedure WebModuleAfterDispatch(Sender: TObject; Request: TWebRequest;
      Response: TWebResponse; var Handled: Boolean);

  private 
    { Private declarations }
  public
    { Public declarations }
  end;

var
  WebModuleClass: TComponentClass = TWebModule1;
  CriticalSection: TCriticalSection;

  const
    
    EncryptionKey='Q9__M_f_qc1EX8ce98HsQrYG_s4aRGQj5G_Ixu4XTQs___esfxdH_kf_lg5_8lnk';

    Procedure StartServer;
    Procedure CloseServer;

implementation

Uses web.WebBroker;

var //these will be accessible to all threads
  ServerStarted:boolean;
  LogType:TLogType;
  LogFile, PGVendorLib:string;
  FThreadFileLog : TThreadFileLog;
  MimeTable:TIDMimeTable;


{%CLASSGROUP 'System.Classes.TPersistent'}

{$R *.dfm}


procedure AddToLog(str: string; whichLogType:TLogType);
  var
    Log:string;
begin
  try
    if (LogType=leNone) or (ord(whichLogType)>ord(LogType)) then //we are not logging these types of events or errors
      exit;
    case whichLogType of
      leCriticalError: Log:='CRITICAL ERROR: ';
      leMajorError: Log:='MAJOR ERROR: ';
      leMinorError: Log:='MINOR ERROR: ';
      leMajorEvents: Log:='MAJOR EVENT: ';
      leDevelopment: Log:='DEVELOPMENT: ';
    end;

    Log:=Log+str;
    FThreadFileLog.Log(LogFile, Log);
  Except
  end;
end;

Procedure CreateMimeTable;
begin
  try
    if not assigned(MimeTable) then
      begin
        MimeTable:=TIDMimeTable.create;
        AddToLog('Mime Table Create', leDevelopment);
      end;
  except
   on E: Exception do
      AddToLog('Error CreateMimeTable: '+e.Message, leMajorError)
  end;
end;


Procedure StartServer;
    var
      appPath:String;
      iniFile:TIniFile;
begin
  if serverStarted then //Probably overkill as should only be called once
    exit;
  CriticalSection.Enter;
  try
    try
      FThreadFileLog := TThreadFileLog.Create();

      appPath:=GetModuleName(HInstance);
      if pos('\\?\', appPath)>0 then //this is something to do with the Windows function GetModuleFileName and uncpaths
        appPath:=copy(appPath, 5, MaxInt);
      iniFile:=TIniFile.Create(changeFileExt(appPath, '.ini'));
      
      try
        if not FileExists(iniFile.FileName) then
          begin
            LogFile:='c:\ProgramData\Log.txt';
            AddToLog('IniFile does not exist at location: "'+iniFile.FileName, leCriticalError);
            Exit;
          end;

        AddToLog('Server Started', leMajorEvents);
        LogType:=TLogType(iniFile.ReadInteger('GENERAL', 'log_type', 1));
        LogFile:=inifile.ReadString('GENERAL', 'log_file', '');
        if (LogType<>leNone) and (LogFile<>'') then
          begin
            if not DirectoryExists(ExtractFilePath(LogFile)) 
            and not ForceDirectories(ExtractFilePath(LogFile)) then            
              LogFile:='c:\ProgramData\Log.txt';             
          end;
        
        PGVendorLib:=inifile.ReadString('LOCATIONS', 'pg_vendor_lib', '');

      finally
        iniFile.Free;
      end;

      AddToLog('LogType:'+inttostr(ord(logType)), leDevelopment);
      AddToLog('LogFile: '+LogFile, leDevelopment);
      AddToLog('FDPhysPGDriverLink.VendorLib: '+PGVendorLib, leDevelopment);
      
      AddToLog('Server Started', leMajorEvents);
      ServerStarted:=true;
    except
      try AddToLog('Server could not be loaded: '+SysErrorMessage(GetLastError), leCriticalError); except end;
    end;
  finally
    CriticalSection.Leave;
  end;
end;

Procedure CloseServer;
begin
 CriticalSection.Enter;
  try
    if assigned(MimeTable) then
      try MimeTable.Free; except end;
    if assigned(FThreadFileLog) then
      try FThreadFileLog.Free; except end;
    try AddToLog('Server Closed', leMajorEvents); except end;
  finally
    CriticalSection.Leave;
  end;
end;

procedure TWebModule1.WebModule1HeadAction(Sender: TObject;
  Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
  var fileName:string;
begin
  Handled:=true;
  try
	//If this action needed the mimetable it would call CreateMimeTable at this point
    FileName:=trim(Request.GetFieldByName('Location'));
    if FileName='' then
      begin
        SetResponse(Response, 400, 'Bad Request', 'Head');
        exit;
      end;


    if fileExists(FileName) then
      SetResponse(Response, 200, 'OK')
    else
      SetResponse(Response, 404, 'NotFound');
  except
  end;
end;

procedure TWebModule1.WebModuleBeforeDispatch(Sender: TObject;
  Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
begin
  FDPhysPgDriverLink.VendorLib:=PGVendorLib;
  AddToLog('Before Dispatch - PathInfo: '+Request.PathInfo, leDevelopment);
  AddToLog('Before Dispatch - Method: '+Request.Method, leDevelopment);
  AddToLog('MaxConnections: : '+Application.MaxConnections.ToString, leDevelopment);
  AddToLog('ActiveConnections: : '+Application.ActiveCount.ToString, leDevelopment);
  AddToLog('InactiveConnections: : '+Application.InActiveCount.ToString, leDevelopment);
  AddToLog('CachedConnections: : '+Ord(Application.CacheConnections).ToString, leDevelopment);
end;

procedure TWebModule1.WebModuleAfterDispatch(Sender: TObject;
  Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
begin
  AddToLog('After Dispatch - Method: '+Request.Method, leDevelopment);
end;

end.

ThreadFileLog unit

unit ThreadFileLog;

interface

uses Windows, ThreadUtilities, System.Classes;

type
    PLogRequest = ^TLogRequest;
    TLogRequest = record
        LogText  : String;
        FileName : String;
    end;

    TThreadFileLog = class(TObject)
    private
        FThreadPool: TThreadPool;
        procedure HandleLogRequest(Data: Pointer; AThread: TThread);
    public
        constructor Create();
        destructor Destroy; override;
        procedure Log(const FileName, LogText: string);
    end;

implementation

uses
  System.SysUtils;



procedure LogToFile(const FileName, LogString: String);
  var
    c1, c2 : dword;
    i:integer;
    s:string;
begin
  try
    c1 := CreateFile(PChar(FileName), GENERIC_READ or GENERIC_WRITE, 0,
    nil, OPEN_ALWAYS, 0, 0);

    i:=0;
    while c1=INVALID_HANDLE_VALUE do
      begin
        if i>200 then //2 secs elapsed and haven't been able to access log
          exit
        else
          begin
            inc(i);
            Sleep(10);
            c1 := CreateFile(PChar(FileName), GENERIC_READ or GENERIC_WRITE, 0,
            nil, OPEN_ALWAYS, 0, 0);
          end;
      end;

  if c1 <> INVALID_HANDLE_VALUE then
    begin
      SetFilePointer(c1, 0, nil, FILE_END);
      S:=DateTimeToStr(Now)+': '+LogString+sLineBreak+SLineBreak;
      WriteFile(c1, s[1], length(s)*SizeOf(Char), c2, nil);
      CloseHandle(c1);
    end
  except
  end;
end;

constructor TThreadFileLog.Create();
begin
    FThreadPool := TThreadPool.Create(HandleLogRequest, 1);
end;

destructor TThreadFileLog.Destroy;
begin
    FThreadPool.Free;
    inherited;
end;

procedure TThreadFileLog.HandleLogRequest(Data: Pointer; AThread: TThread);
var
    Request: PLogRequest;
begin
    Request := Data;
    try
        LogToFile(Request^.FileName, Request^.LogText);
    finally
      Dispose(Request);
    end;
end;

procedure TThreadFileLog.Log(const FileName, LogText: string);
var
    Request: PLogRequest;
begin
    New(Request);
    Request^.LogText  := LogText;
    Request^.FileName := FileName;
    FThreadPool.Add(Request);
end;

end.

Other possible useful information:

 

The Application

The application created in the project file is a TWebApplication not a TApplication.

It exposes MaxConnections, ActiveConnections and InActiveConnections, CachedConnection.

 

MaxConnections

The maximum no of connections to your DLL. By default this is 32. So up to 32 new threads will be spawned (but note comments re NumberOfThreads below). You can increase or decrease this number as requi

ActiveConnections

This reports the number of connections currently in use.

 

InactiveConnections

If CachedConnections is true new threads are not disposed of after use, but cached for future use. whilst cached they are recorded as InactiveConnections.

 

ISAPIThreadPool unit

This is added by default and deals with threading.

 

NumberOfThreads

Global variable declared in the ISAPIThreadPool unit. It is set by default to 25. So whilst the default MaxConnections is 32 (so you can theoretically have 32 connections) unless you increase the NumberOfThreads you will in effect be allowed only 25 connections! The example project code deals with this.

 

  • Like 1

Share this post


Link to post

I'm doing something similar and encountered some of the same issues.

 

1. Since ISAPI is a DLL, did you add "IsMultiThread := True;" to let the memory manager know this DLL is using multiple threads (otherwise you can get random-seeming crashes)?

2. I'm not sure your logging code is thread safe, it might be useful to add a critical section to your log writes.

3. There should be no problem using global read-only variables across threads as long as these variables are not classes that do things in the background.

4. If you're accessing a database, make sure to use connection pooling.

5. Do you get a ~90sec freeze when trying to stop the application pool? Make sure to terminate the ISAPI dll correctly.

 

This is my original thread in which the nice people of this forum helped me identify some of these issues:

 

Share this post


Link to post

Thanks for this information. I will go through it all asap. Likely, I will have some questions if you don't mind. Bit of a newbie to ISAPI!

Share this post


Link to post
On 10/16/2019 at 3:15 PM, Yaron said:

This is my original thread in which the nice people of this forum helped me identify some of these issues:

@Yaron It's taken me a while to revisit this and as usual it has not disappointed: days thrown into the abyss trying to change the structure of my ISAPI DLL!

 

I have implemented points 1 to 4 mentioned by Yaron. Point 5 is causing me a problem. I am having a problem with the closing down of the application pool. Yes I get a long delay and my closing section does not fire.

 

My dpr now looks like this:

library MWCWebServer;

uses
  Winapi.ActiveX,
  System.Win.ComObj,
  System.SysUtils,
  Web.WebBroker,
  Web.Win.ISAPIApp,
  Web.Win.ISAPIThreadPool,
  WinAPI.Isapi2,
  WinAPI.Windows,
  System.SyncObjs,
  system.classes,
  WebModule in 'WebModule.pas' {WebModule1: TWebModule};

{$R *.res}



function GetExtensionVersion(var Ver: THSE_VERSION_INFO): BOOL; stdcall;
begin
  Result := Web.Win.ISAPIApp.GetExtensionVersion(Ver);
  CriticalSection := TCriticalSection.Create;
  StartServer;
end;

{I have removed the call to TerminateVersion as it wasn't firing
function TerminateVersion(var Ver: THSE_VERSION_INFO): BOOL; stdcall;
begin
  Result := Web.Win.ISAPIApp.GetExtensionVersion(Ver);
  CloseServer;
  CriticalSection.Free;
end;}

{I have added this procedure as shown in the linked post provied by Yaron}
procedure DoTerminate;
begin
  CloseServer;
  CriticalSection.Free;
end;

exports
  GetExtensionVersion,
  HttpExtensionProc,
  TerminateExtension;


begin
  CoInitFlags := COINIT_MULTITHREADED;
  Application.Initialize;
  Application.WebModuleClass := WebModuleClass;  
  Application.MaxConnections:=200; //MaxConnection=32 by default;
  IsMultiThread:=true;
  TISAPIApplication(Application).OnTerminate:=DoTerminate;
  {Application.CacheConnections:=true;} //NB not necessary as cached by default
  Application.Run;
end.

 

My call to CloseServer now looks as follows:

Procedure CloseServer;
begin 
 CriticalSection.Enter;
  try
    if assigned(MimeTable) then
      try MimeTable.Free; except end;

    FDManager.Close;
    try FDManager.Free; except end;
    try AddToLog('Connection manager deactivated', leMajorEvents); except end;

    try AddToLog('Server Closed', leMajorEvents); except end;

    if assigned(FThreadFileLog) then
      try FThreadFileLog.Free; except end;

  finally
    CriticalSection.Leave;
  end;
end;

CloseServer gets called on shut down as expected. However, it hangs  at "try FDManager.Free; except end;" It appears to execute the line of code above (ie to close FDMAnager), but it does not appear to execute the call to free FDManager not does it it get caught by the try except nor by the try finally end. It just hangs at the call to free. And this is when the application pool stalls in its shutdown process.

 

Does anyone have any ideas why?

Share this post


Link to post

I'm not sure why do you need a critical session for freeing something? Do you think other threads would still using it? And you just freeing it?

Just asking.

Edited by Attila Kovacs

Share this post


Link to post
10 hours ago, Attila Kovacs said:

I'm not sure why do you need a critical session for freeing something? Do you think other threads would still using it? And you just freeing it?

Just asking.

A good question! There is no need. Removed. But that's not the cause of the problem on shutdown. Any ideas why it would be hanging at this point?

Share this post


Link to post

ok

-what if you remove the .free? Will it leak memory or does it get freed when IIS unloads the dll?

-what if you create your FDManager with "TISAPIApplication(Application)" as owner? It's a TComponent descendant so it will free FDManager.

Share this post


Link to post
28 minutes ago, Attila Kovacs said:

ok

-what if you remove the .free? Will it leak memory or does it get freed when IIS unloads the dll?

-what if you create your FDManager with "TISAPIApplication(Application)" as owner? It's a TComponent descendant so it will free FDManager.

Further process of elimination. It was nothing to do with the call to free FDManager. It was the AddToLog calls that were causing the problem. These are threaded using the FThreadFileLog, which I free in the final part of CloseServer and before the threads called in CloseServer had terminated. I have changed these calls so that they are now not threaded and also as a double precaution added a check in ThreadFileLog's destroy event to make sure  no threads are running before it is shut down.

 

My CloseServer procedure now frees all the resources as expected, but the application pool is still taking an age to close down. There must be some resource I am creating and not freeing. I will check all my code and if I can pinpoint what it is I will update.

  • Like 1

Share this post


Link to post
5 minutes ago, Mark Williams said:

application pool is still taking an age to close down

 

How can you see that? Hourglass, or the time until the dll is released?

 

Share this post


Link to post
Just now, Attila Kovacs said:

 

How can you see that? Hourglass, or the time until the dll is released?

 

Terminate WWW via services. I can see my logfile is updated almost immediately with message "Server Closed" and then the progress bar from Services takes well over a minute from this point to complete the shutdown.

Share this post


Link to post
12 minutes ago, Attila Kovacs said:

Ok. How do you create FDManager (Create(nil)?, not that you free it twice) and where is the var declared?

 

See post two up. I don't think the problem is anything to do with FDManager. The reason it was hanging at the point of freeing was due to the call to the threadlog and my freeing of the threadlog before the threaded call had been made. My CloseServer now executes fully and appears to free all resources its requested to free. There has to be something else I am missing. Something I am creating somewhere and not freeing. No idea what though! Checking now.

Share this post


Link to post

Ok.

- I'll assume then that you are aware that in case of TFDManager.Create(SomeComponent), including placing the component on a datamodule, FDManager will be free'd if SomeComponent is free'd.

- I'm sure, that you have tried commenting out the whole OnTerminate, and IIS still stalls on stop.

Let me know if you find something. I'm really curious.

Share this post


Link to post
16 minutes ago, Attila Kovacs said:

- I'll assume then that you are aware that in case of TFDManager.Create(SomeComponent), including placing the component on a datamodule, FDManager will be free'd if SomeComponent is free'd.

Yes. I'm sure that freeing of FDManager is not the issue.

 

17 minutes ago, Attila Kovacs said:

- I'm sure, that you have tried commenting out the whole OnTerminate, and IIS still stalls on stop.

Yes.

 

17 minutes ago, Attila Kovacs said:

Let me know if you find something. I'm really curious.

Will do.

  • Thanks 1

Share this post


Link to post

I've been through all my code. As far as I can see, I am destroying everything I create in an action event within the same action event.

 

I have tried calling the DefaultHandlerAction and nothing else.

 

procedure TWebModule1.WebModule1DefaultHandlerAction(Sender: TObject;
  Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
begin
  Handled:=true;
  try
    SetResponse(Response, 400, 'Bad Request', 'Default Handler - if you are getting this message it either means that your are providing the wrong PathInfo or the action has been deleted.');
  except
  end;
end;

And SetResponse:

 

Procedure SetResponse(var Response:TWebResponse; code:integer; statusText:String; debug:string='');
begin
  Response.StatusCode:=code;
  Response.ReasonString:=code.ToString+' '+StatusText;
  if debug<>'' then
    AddToLog('Response set: '+Response.StatusCode.ToString+' '+StatusText+' Debug Info: '+debug, leMinorError)
  else
    AddToLog('Response set: '+Response.StatusCode.ToString+' '+StatusText, leDevelopment);
end;

It still hangs on shutdown.

 

So I tried removing my calls to StartServer and CloseServer. Edited my DefaulHandlerAction so it didn't call SetResponse. It just sets the statuscode to 400 and still a huge delay in shutdown.

 

My project file now  looks like this:

 

library MWCWebServer;

uses
  Winapi.ActiveX,
  System.Win.ComObj,
  System.SysUtils,
  Web.WebBroker,
  Web.Win.ISAPIApp,
  Web.Win.ISAPIThreadPool,
  WinAPI.Isapi2,
  WinAPI.Windows,
  System.SyncObjs,
  system.classes,
  WebModule in 'WebModule.pas' {WebModule1: TWebModule};

{$R *.res}

exports
  GetExtensionVersion,
  HttpExtensionProc,
  TerminateExtension;


begin
  CoInitFlags := COINIT_MULTITHREADED;
  Application.Initialize;
  Application.WebModuleClass := WebModuleClass;
  Application.MaxConnections:=200; //MaxConnection=32 by default;
  IsMultiThread:=true;  
  Application.Run;
end.

I'm not creating anything over and above the webapplication itself and still it won't shut down properly.

 

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×