Mark Williams 14 Posted September 3, 2019 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
stijnsanders 35 Posted September 3, 2019 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
Mark Williams 14 Posted September 3, 2019 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
Mark Williams 14 Posted September 3, 2019 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
Attila Kovacs 629 Posted September 3, 2019 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
Mark Williams 14 Posted September 3, 2019 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
Attila Kovacs 629 Posted September 3, 2019 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
Attila Kovacs 629 Posted September 3, 2019 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. 1 Share this post Link to post
Mark Williams 14 Posted September 5, 2019 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. 1 Share this post Link to post
Yaron 53 Posted October 16, 2019 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
Mark Williams 14 Posted October 16, 2019 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
Mark Williams 14 Posted January 26, 2020 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
Attila Kovacs 629 Posted January 26, 2020 (edited) 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 January 26, 2020 by Attila Kovacs Share this post Link to post
Mark Williams 14 Posted January 27, 2020 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
Attila Kovacs 629 Posted January 27, 2020 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
Attila Kovacs 629 Posted January 27, 2020 sidenote: in "TWebApplication.Create" there is already a "if IsLibrary then IsMultiThread := True;" Share this post Link to post
Mark Williams 14 Posted January 27, 2020 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. 1 Share this post Link to post
Attila Kovacs 629 Posted January 27, 2020 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
Mark Williams 14 Posted January 27, 2020 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
Attila Kovacs 629 Posted January 27, 2020 Ok. How do you create FDManager (Create(nil)?, not that you free it twice) and where is the var declared? Share this post Link to post
Mark Williams 14 Posted January 27, 2020 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
Attila Kovacs 629 Posted January 27, 2020 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
Mark Williams 14 Posted January 27, 2020 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. 1 Share this post Link to post
Mark Williams 14 Posted January 27, 2020 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