thornton_paul
asked on
How do I write to a database in an Indy TIdTCPServer, TIdCommandHandler event without thread conflicts?
Hi,
I've written a client server/database application that works beautifully
with a single client. Unfortunately, when another client connects, the
server freezes after a few requests. Here's how it's set up:
- Server event (Handler) fires when a client sends a command.
- A database connection is obtained from a pool of connections (done within
a critical section)
- The database is queried <--- This is where the problem occurs!!!
- The database connection is released back to the pool of connections
The application gets stuck in "IdServerIOHandlerSocket", line 50:
.
.
while not AThread.Stopped do begin
if LIOHandler.Binding.Select( ASocket, 100) then begin // Wait for 100
ms <--- Gets stuck here!
if LIOHandler.Binding.Accept( ASocket) then begin
.
.
If I remove the database query, everything works fine. If I wrap the entire
event in a critical section, everything is OK.
I know I must be doing something wrong here. Probably some basic thread
guideline that I'm not adhering to. My question is, how do people normally
write to databases within Indy events? Are there any example projects?
Please bear in mind that I am relatively new to Indy.
I'm using Delphi 7 Arch, Indy 9 and FIBPlus components connecting to a
Firebird 1.5 database.
If anybody thinks it will help, I may be able to send them some of the
source code.
Many Thanks
Paul Thornton
I've written a client server/database application that works beautifully
with a single client. Unfortunately, when another client connects, the
server freezes after a few requests. Here's how it's set up:
- Server event (Handler) fires when a client sends a command.
- A database connection is obtained from a pool of connections (done within
a critical section)
- The database is queried <--- This is where the problem occurs!!!
- The database connection is released back to the pool of connections
The application gets stuck in "IdServerIOHandlerSocket",
.
.
while not AThread.Stopped do begin
if LIOHandler.Binding.Select(
ms <--- Gets stuck here!
if LIOHandler.Binding.Accept(
.
.
If I remove the database query, everything works fine. If I wrap the entire
event in a critical section, everything is OK.
I know I must be doing something wrong here. Probably some basic thread
guideline that I'm not adhering to. My question is, how do people normally
write to databases within Indy events? Are there any example projects?
Please bear in mind that I am relatively new to Indy.
I'm using Delphi 7 Arch, Indy 9 and FIBPlus components connecting to a
Firebird 1.5 database.
If anybody thinks it will help, I may be able to send them some of the
source code.
Many Thanks
Paul Thornton
ASKER
I am using a DB Connection pool that uses multiple Connection objects. Each object has it's own database connection and dataset (query) components.
I think the problem lmay lie in the fact that I'm obtaining the connection from within the Indy event. Is this not possible due to the Indy's multiple threads? Do I have to use something like SendMessage? If so, could you give a quick example?
I think the problem lmay lie in the fact that I'm obtaining the connection from within the Indy event. Is this not possible due to the Indy's multiple threads? Do I have to use something like SendMessage? If so, could you give a quick example?
something like:
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, IdBaseComponent, IdComponent, IdTCPServer;
const
WM_HANDLEDB = WM_USER + 123;
type
TForm1 = class(TForm)
IdTCPServer1: TIdTCPServer;
procedure IdTCPServer1Execute(AThrea d: TIdPeerThread);
private
{ Private declarations }
protected
procedure WMHandleDB(var AMsg: TMessage); message WM_HANDLEDB;
public
{ Public declarations }
end;
PDBRequestInfo = ^TDBRequestInfo;
TDBRequestInfo = record
Request: Integer;
Query: TIBQuery;
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.IdTCPServer1Execute (AThread: TIdPeerThread);
var dbri: TDBRequestInfo;
begin
// handle the data
// now get a query
dbri.Request:=1; // get query
case SendMessage(Handle, WM_HANDLEDB, 1, Integer(@dbri)) of
1: begin // if we got a query
// use that query - in this connections thread
dbri.Query.SQL:='select * from table';
end;
else
begin
// unknown result
end;
end;
end;
procedure TForm1.WMHandleDB(var AMsg: TMessage);
var dbri: TDBRequestInfo;
begin
// forms thread - main thread
case AMsg.WParam of
1: begin
dbri:=PDBRequestInfo(AMsg. LParam)^;
// get our query from the pool
dbri.Query:=QueryPool.GetQ uery;
// return message result
AMsg.Result:=Integer(Assig ned(dbri.Q uery));
end;
end;
end;
end.
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, IdBaseComponent, IdComponent, IdTCPServer;
const
WM_HANDLEDB = WM_USER + 123;
type
TForm1 = class(TForm)
IdTCPServer1: TIdTCPServer;
procedure IdTCPServer1Execute(AThrea
private
{ Private declarations }
protected
procedure WMHandleDB(var AMsg: TMessage); message WM_HANDLEDB;
public
{ Public declarations }
end;
PDBRequestInfo = ^TDBRequestInfo;
TDBRequestInfo = record
Request: Integer;
Query: TIBQuery;
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.IdTCPServer1Execute
var dbri: TDBRequestInfo;
begin
// handle the data
// now get a query
dbri.Request:=1; // get query
case SendMessage(Handle, WM_HANDLEDB, 1, Integer(@dbri)) of
1: begin // if we got a query
// use that query - in this connections thread
dbri.Query.SQL:='select * from table';
end;
else
begin
// unknown result
end;
end;
end;
procedure TForm1.WMHandleDB(var AMsg: TMessage);
var dbri: TDBRequestInfo;
begin
// forms thread - main thread
case AMsg.WParam of
1: begin
dbri:=PDBRequestInfo(AMsg.
// get our query from the pool
dbri.Query:=QueryPool.GetQ
// return message result
AMsg.Result:=Integer(Assig
end;
end;
end;
end.
sorry .. missunderstood
when the Query is executed the problems arise
maybe it's a FIBPlus issue .. I didn't experience this with dbExpress
although I'm not using a db pool, I create a connection inside the calling thread (in an Intraweb app - UserSession)
when the Query is executed the problems arise
maybe it's a FIBPlus issue .. I didn't experience this with dbExpress
although I'm not using a db pool, I create a connection inside the calling thread (in an Intraweb app - UserSession)
ASKER
That's right, but I guess it could possibly be something to do with the way in which I retrieve the DB connection.
Can you explain what you actually gain by using a message instead of a simple function call? Sorry it it's a stupid question!
Can you explain what you actually gain by using a message instead of a simple function call? Sorry it it's a stupid question!
if you call a method of an object that was created in another thread it's still executed in the caller thread
SendMessage call is handled by the receiving window in it's thread
the thread that calls SendMessage waits until the receiving window(thread) handles the message and returns
SendMessage call is handled by the receiving window in it's thread
the thread that calls SendMessage waits until the receiving window(thread) handles the message and returns
ASKER
My equivalent of your "WMHandleDB" procedure is this:
function TDBConnectionPool.GetConne ction: TBaseDBConnection;
var
i: Integer;
begin
// Get an available connection
// In a critical section so only one thread at a time can claim a connection.
AcquireConnection.Acquire;
try
Result := nil;
// Find a free connection
for i := 0 to pred(FDBConnections.Count) do
if not TBaseDBConnection(FDBConne ctions[i]) .InUse then
begin
Result := TBaseDBConnection(FDBConne ctions[i]) ;
Break;
end;
// If no free connections exist, create one
if not Assigned(Result) then
begin
Result := CreateConnection;
Result.FTempName := IntToStr(i);
FDBConnections.Add(Result) ;
end;
Result.Acquire;
// if we got a connection, make sure it's connected
if Assigned(Result) then
Result.Connect;
finally
AcquireConnection.Release;
end;
end;
function TDBConnectionPool.CreateCo nnection: TBaseDBConnection;
begin
Result := nil;
// Create a new connection
{$IFDEF FBCONNECTION}
if FDatabaseType = dtFirebird then
Result := TFirebirdDBConnection.Crea te;
{$ENDIF}
// Exception raised if no connection created
if Assigned(Result) then
begin
Result.FDatabase := FDatabase;
Result.FUsername := FUsername;
Result.FPassword := FPassword;
end
else
raise Exception.Create('Database Type not implemented');
end;
The DBConnectionPool component is created on the main form of the application. Would the critical section not be the equivalent of calling the method via a Message?
function TDBConnectionPool.GetConne
var
i: Integer;
begin
// Get an available connection
// In a critical section so only one thread at a time can claim a connection.
AcquireConnection.Acquire;
try
Result := nil;
// Find a free connection
for i := 0 to pred(FDBConnections.Count)
if not TBaseDBConnection(FDBConne
begin
Result := TBaseDBConnection(FDBConne
Break;
end;
// If no free connections exist, create one
if not Assigned(Result) then
begin
Result := CreateConnection;
Result.FTempName := IntToStr(i);
FDBConnections.Add(Result)
end;
Result.Acquire;
// if we got a connection, make sure it's connected
if Assigned(Result) then
Result.Connect;
finally
AcquireConnection.Release;
end;
end;
function TDBConnectionPool.CreateCo
begin
Result := nil;
// Create a new connection
{$IFDEF FBCONNECTION}
if FDatabaseType = dtFirebird then
Result := TFirebirdDBConnection.Crea
{$ENDIF}
// Exception raised if no connection created
if Assigned(Result) then
begin
Result.FDatabase := FDatabase;
Result.FUsername := FUsername;
Result.FPassword := FPassword;
end
else
raise Exception.Create('Database
end;
The DBConnectionPool component is created on the main form of the application. Would the critical section not be the equivalent of calling the method via a Message?
>>Would the critical section not be the equivalent of calling the method via a Message?
nope, a CS merely locks other threads from executing that part of code
the message handler executes in the other thread and blocks the calling thread - and all others ofcourse
your problem is as I suspected - you have one Database
if you want to keep it like this you'll have to synch/protect all the calls to the DB
another option is to create a T(IB)Database for every pooled connection - I prefer this method - you limit the pool size anyway :-)
nope, a CS merely locks other threads from executing that part of code
the message handler executes in the other thread and blocks the calling thread - and all others ofcourse
your problem is as I suspected - you have one Database
if you want to keep it like this you'll have to synch/protect all the calls to the DB
another option is to create a T(IB)Database for every pooled connection - I prefer this method - you limit the pool size anyway :-)
ASKER
I am creating a seperate database for every connection. Here's part of the code of a TBaseDBConnection object:
//------------------------ ---------- ---------- ---------- ---------- ---------- --------
{ TFirebirdDBConnection }
//------------------------ ---------- ---------- ---------- ---------- ---------- --------
constructor TFirebirdDBConnection.Crea te;
begin
inherited;
// Create the database components
FDatabaseConnection := TpFIBDatabase.Create(nil);
FTransaction := TpFIBTransaction.Create(ni l);
FQuery := TpFIBDataSet.Create(nil);
FStoredProcedure := TpFIBStoredProc.Create(nil );
end;
procedure TFirebirdDBConnection.Conn ect;
begin
if not FDatabaseConnection.Connec ted then
begin
// Set up the database component
FDatabaseConnection.Databa seName := Database;
FDatabaseConnection.DBPara ms.Clear;
FDatabaseConnection.DBPara ms.Add(for mat('user_ name=%s', [FUsername]));
FDatabaseConnection.DBPara ms.Add(for mat('passw ord=%s', [FPassword]));
FDatabaseConnection.SQLDia lect := 3;
// Set up the Transaction component
FTransaction.DefaultDataba se := FDatabaseConnection;
FDatabaseConnection.Defaul tTransacti on := FTransaction;
// Set up the Query component
FQuery.Database := FDatabaseConnection;
FQuery.PrepareOptions := FQuery.PrepareOptions + [psAskRecordCount];
// Set up the Stored Procedure component
FStoredProcedure.Database := FDatabaseConnection;
// Open the database connection
FDatabaseConnection.Open;
end;
end;
//------------------------ ---------- ---------- ---------- ---------- ---------- --------
I can try rewriting the code to utilise Messages. I'll let you know outcome. Any other ideas before I start?
//------------------------
{ TFirebirdDBConnection }
//------------------------
constructor TFirebirdDBConnection.Crea
begin
inherited;
// Create the database components
FDatabaseConnection := TpFIBDatabase.Create(nil);
FTransaction := TpFIBTransaction.Create(ni
FQuery := TpFIBDataSet.Create(nil);
FStoredProcedure := TpFIBStoredProc.Create(nil
end;
procedure TFirebirdDBConnection.Conn
begin
if not FDatabaseConnection.Connec
begin
// Set up the database component
FDatabaseConnection.Databa
FDatabaseConnection.DBPara
FDatabaseConnection.DBPara
FDatabaseConnection.DBPara
FDatabaseConnection.SQLDia
// Set up the Transaction component
FTransaction.DefaultDataba
FDatabaseConnection.Defaul
// Set up the Query component
FQuery.Database := FDatabaseConnection;
FQuery.PrepareOptions := FQuery.PrepareOptions + [psAskRecordCount];
// Set up the Stored Procedure component
FStoredProcedure.Database := FDatabaseConnection;
// Open the database connection
FDatabaseConnection.Open;
end;
end;
//------------------------
I can try rewriting the code to utilise Messages. I'll let you know outcome. Any other ideas before I start?
using messages won't help in this case unless you handle the Query execution .. but you can do that with your existing lock and as you said it works
I don't have FIBPlus and can't check .. also don't have the time for that :-/
I don't have FIBPlus and can't check .. also don't have the time for that :-/
ASKER
How do you mean "handle the Query execution"?
Also, if I put the query in a critical section surely only one thread can execute it at a time. This kind of negates the point of using threads. I feel like I'm missing something here! What I'm looking to do is run concurrent database queries. i.e. one thread can be waiting for a long running query to finish while another does a query at the same time.
Also, how would I use the Message code if I'm running it in a TComponent that has no Handle property?
Thanks for your great help so far.
Also, if I put the query in a critical section surely only one thread can execute it at a time. This kind of negates the point of using threads. I feel like I'm missing something here! What I'm looking to do is run concurrent database queries. i.e. one thread can be waiting for a long running query to finish while another does a query at the same time.
Also, how would I use the Message code if I'm running it in a TComponent that has no Handle property?
Thanks for your great help so far.
try that DB pooling with dbExpress or some other DB compos and see if they have the same problems
or .. create/destroy all db stuff inside the servers OnConnect/OnDisconnect methods
personaly I'd create a TIdPeerThread descendant with your TBaseDBConnection as a property
you set the servers ThreadClass:=TMyPeerThread ;
then in the events you cast the AThread param to your thread class
if you have a stateless system then this might be a bit of an overhead
can I suggest you check out RemObject - www.remobjects.com
I've been using the V2 for over half a year now and it works great .. a real must have for any serious communication
V3 has lots of new goodies .. which I still have to check out :)
about messages
you create a window and a WndProc method like:
//create
var FHandle: Cardinal = AllocateHWnd(WndProc);
// free
DeallocateHWnd(FHandle);
procedure TMyClass.WndProc(var AMsg: TMessage);
begin
case AMsg.Msg of
...
else AMsg.Result:=DefWindowProc (FHandle, AMsg.Msg, AMsg.WParam, AMsg.LParam);
end;
end;
or .. create/destroy all db stuff inside the servers OnConnect/OnDisconnect methods
personaly I'd create a TIdPeerThread descendant with your TBaseDBConnection as a property
you set the servers ThreadClass:=TMyPeerThread
then in the events you cast the AThread param to your thread class
if you have a stateless system then this might be a bit of an overhead
can I suggest you check out RemObject - www.remobjects.com
I've been using the V2 for over half a year now and it works great .. a real must have for any serious communication
V3 has lots of new goodies .. which I still have to check out :)
about messages
you create a window and a WndProc method like:
//create
var FHandle: Cardinal = AllocateHWnd(WndProc);
// free
DeallocateHWnd(FHandle);
procedure TMyClass.WndProc(var AMsg: TMessage);
begin
case AMsg.Msg of
...
else AMsg.Result:=DefWindowProc
end;
end;
ASKER
Hi Lee,
I've now done the following:
- Created a new TIdPeerThread descendant that contains a DB Connection and Query.
- Removed FIBPlus and replaced it with DBExpress.
Unfortunately, I am still experiencing exactly the same problem.
In order to try to sort this out, I've written a very small application that demonstrates the problem with all the clutter removed. It's only 500K including the executables. Would it be possible for you (or anybody else that wants the points) to download it and try to figure out where the problem lies. I'm pulling my hair out here!!! :o(
You can download the application source here: http:\\www.helion-studios.com\download\ThreadTest.zip
To compile the application you need Delphi 7 (Indy 9 and DBExpress). You will also need either Firebird or Interbase installed although it would be very simple to convert it to another database as it's now using DBExpress. There is a Firebird database included in the directory. It has one table: "ALERTS". The table contains one field: "ALERT_NAME" String(30). That's it. Just complile and run.
When compiled, start a single server application then start a single client application. In the client application, click "Connect" (it will try to connect to the server on the local [127.0.0.1] machine). Click "Continuous Send". You will see a lot of "Data Sent", "Data Received" messages scrolling up the memo. Now, try opening another client and do exactly the same thing. After a few moments both clients will lock up as they are waiting for a response from the server that never arrives. Why?
Thanks for your help with this very frustrating problem.
/Paul
I've now done the following:
- Created a new TIdPeerThread descendant that contains a DB Connection and Query.
- Removed FIBPlus and replaced it with DBExpress.
Unfortunately, I am still experiencing exactly the same problem.
In order to try to sort this out, I've written a very small application that demonstrates the problem with all the clutter removed. It's only 500K including the executables. Would it be possible for you (or anybody else that wants the points) to download it and try to figure out where the problem lies. I'm pulling my hair out here!!! :o(
You can download the application source here: http:\\www.helion-studios.com\download\ThreadTest.zip
To compile the application you need Delphi 7 (Indy 9 and DBExpress). You will also need either Firebird or Interbase installed although it would be very simple to convert it to another database as it's now using DBExpress. There is a Firebird database included in the directory. It has one table: "ALERTS". The table contains one field: "ALERT_NAME" String(30). That's it. Just complile and run.
When compiled, start a single server application then start a single client application. In the client application, click "Connect" (it will try to connect to the server on the local [127.0.0.1] machine). Click "Continuous Send". You will see a lot of "Data Sent", "Data Received" messages scrolling up the memo. Now, try opening another client and do exactly the same thing. After a few moments both clients will lock up as they are waiting for a response from the server that never arrives. Why?
Thanks for your help with this very frustrating problem.
/Paul
ASKER CERTIFIED SOLUTION
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
ASKER
Don't have time to do anything now (23:50 here in Norway), but I'll take a look at work tomorrow.
Thanks for all your effort :o) Hopefully this will fix the problem.
Thanks for all your effort :o) Hopefully this will fix the problem.
ASKER
Hi Lee,
I've tried the code above (and turned off CommandHandlersEnabled). It certainly runs better. With two clients it runs for a long time, but with three or four it locks up pretty quick.
I'm still using the same version of Indy however. I'm going to try the latest version, but need the Delphi 7 CD to uninstall the old version. I'm having this delivered to me at 12 today. When I get it I'll try it with the latest version of Indy.
During the meanwhilst, would it be possible to email the executables to me at "norgepaul at hotmail dot com"? This way I can double check that the executable that work on your machine work on mine too.
Cheers
I've tried the code above (and turned off CommandHandlersEnabled). It certainly runs better. With two clients it runs for a long time, but with three or four it locks up pretty quick.
I'm still using the same version of Indy however. I'm going to try the latest version, but need the Delphi 7 CD to uninstall the old version. I'm having this delivered to me at 12 today. When I get it I'll try it with the latest version of Indy.
During the meanwhilst, would it be possible to email the executables to me at "norgepaul at hotmail dot com"? This way I can double check that the executable that work on your machine work on mine too.
Cheers
sure, when I get home
ASKER
Here's something a little strange:
I tried running the sample application with Embedded Firebird and got the same results as you i.e. 4 clients running happily for as long as I left them!!!
Looks like this could be a Firebird C/S problem. Can't believe that somebody's not found it before now though.
I tried running the sample application with Embedded Firebird and got the same results as you i.e. 4 clients running happily for as long as I left them!!!
Looks like this could be a Firebird C/S problem. Can't believe that somebody's not found it before now though.
I thought of that as well but didn't test it with FB Server
try with new Indy and see if that makes a difference
maybe even try with Indy10, it's very stable now .. not to mention it performs 4x better (with SuperCore enabled) than Indy 9 ;-)
try with new Indy and see if that makes a difference
maybe even try with Indy10, it's very stable now .. not to mention it performs 4x better (with SuperCore enabled) than Indy 9 ;-)
here are the binaries using my method: http://files.delphi-si.com/lee/ThreadTestBin.zip
the db should be in the same dir
the db should be in the same dir
ASKER
Hi Lee,
Looks like the mystery is solved. The problem is to do with the way in which Frirebird connects to the database. If I use the connection string:
"c:\Database.fdb"
then the locking problem occurs. If however the string is:
"127.0.0.1:c:\Database.fdb "
the deadlock goes away. Apparently it is to do with a problem in Firebirds "Local" connectivity.
Thanks for all your help and patience. You certainly pointed me in the right direction and showed me a thing or two about threads.
You get the points. Go crazy :o)
Cheers,
Paul
Looks like the mystery is solved. The problem is to do with the way in which Frirebird connects to the database. If I use the connection string:
"c:\Database.fdb"
then the locking problem occurs. If however the string is:
"127.0.0.1:c:\Database.fdb
the deadlock goes away. Apparently it is to do with a problem in Firebirds "Local" connectivity.
Thanks for all your help and patience. You certainly pointed me in the right direction and showed me a thing or two about threads.
You get the points. Go crazy :o)
Cheers,
Paul
!! I should've thought of that -- had a similar problem
yes .. using TCP/IP with the server solves that, otherwise it must be singlethreaded - that's even documented :)
yes .. using TCP/IP with the server solves that, otherwise it must be singlethreaded - that's even documented :)
if that's the case then you must either provide a unique connection component for each thread or synchronise the calls
I've done both and both works just fine
I did the synching with SendMessage - like: SendMessage(Handle, WM_HANDLEDBSTUFF, DB_ACTION, Integer(DBObjectInfo));
don't know if it's a FIBPlus issue - I'm using dbExpress
when I used one connection for all datasets without synching I mostly got an "5 - Access denied" error