I'm writing a Datasnap application with a TCP connection between client/server, with the server connected to an SQL server.
Server has a datamodule DM1 with all dataset queries and SQL connection. The DM1 also has REST request/client/response components.
DM1 has an exposed function PostDataAsync with ID param: to generate a json from a dataset, then HTTP post it to a RESTFul service. It returns the number of records failed to post in the callback arg.
The DSServer of this DM1 is “Invocation”.
The invocation server type should ensure that each server method call has its own DB connection, dataset, Rest components, which will not have multiple calls interfere with the data of each other (if added parallel threading).
procedure TServerMethods1.postCustOrderHistAsync(CustomerID: String; callback: TDBXcallback);
var
jsonObject: TJSONObject;
CallbackValue: TJsonValue;
errors: Integer;
begin
errors := postCustOrderHist(CustomerID); //takes time to post, returns num of failed records
jsonObject := TJSONObject.create;
jsonObject.AddPair(tjsonpair.create('errors', errors.ToString));
CallbackValue := callback.Execute(jsonObject);
end;
Client has a button which calls the server method PostDataAsync with ID param, and also a callback function “ShowNotification” (which uses windows notification center to show Post notification status).
For now, the application works as following: the client calls the server function synchronously, that means the main thread waits for the server function to finish the HTTP post and then runs the callback notification; the client hanging meanwhile.
TDSCallbackWithMethod = class(TDBXCallback)
private
FCallbackMethod: TDSCallbackMethod;
public
constructor Create(ACallbackMethod: TDSCallbackMethod);
function Execute(const Args: TJSONValue): TJSONValue; override; //executes FCallbackMethod
end;
procedure TMainForm.BtnPostOrderHistoryClick(Sender: TObject);
var
callback: TDBXCallback;
ServerMethods1Client: TServerMethods1Client;
begin
//Define Callback to show notification
callback := TDSCallbackWithMethod.Create(
function(const Args: TJSONValue): TJSONValue
var
errors: integer;
begin
errors := Args.GetValue<integer>('errors');
if errors = 0 then
showNotification(StrSentSuccessfully)
else
showNotification(StrSendingFailed + '(' + errors.ToString + ' not sent)');
result := TJsonTrue.Create;
end);
//Call Server Method
ServerMethods1Client := TServerMethods1Client.Create(DMServerConnection.SQLConnection1.DBXConnection);
try
ServerMethods1Client.postCustOrderHistAsync(EditCustomerId.Text, callback)
finally
ServerMethods1Client.Free;
end;
end;
How should the design be in order to call the server methods asynchronously, and let the server run the callback when done? Post function should be able to be called several times with the same user or multiple simultaneously.
Should the thread be on the server side or Client side? If anybody can help with this, I can send a demo of the application using the Northwind Database.
Note: I have tried running the client function call in a TTask, it works when the user runs the function once at a time. But when the server method is run several times simultaneously, I get a “DBXError…Read error…callback expecting X got Y”. It seems while the client waits for the response callback format from the first request, it gets confused with other tcp protocol packets initiated from the second request. I have tried running ttask at the server side, I get an exception "TOLEDBCommand.Destroy - interfaces not released"
Check out this example, it walks through the steps to create a callback. Basically you need a TDSClientCallbackChannelManager (component) and its RegisterCallback function to tell the datasnap client what method (of object inherited from TDBXCallback) to call on the client side when the callback is fired from the server. You will need to pass the clients session ID's to the server so it can call the correct client with the NotifyCallBack. Then from that callback method you can do what you need to, in a TThread.Queue for safety. You will probably need to create some sort of unique identifier (or maybe your CustomerID will work) in the JSON the sever returns so your client knows which call is the result of which.
For simplifying the client side server method call, I removed the Callback from the client, and just created parallel threads that wait for the response of the server. I still got the same error “DBXError…Read error…callback expecting X got Y”. So that's when I knew that the error wasn't a callback issue, it's an interference between the threads. It turned out that when I was creating the client's proxy methods, all the threads where using the same instance of DBXConnection. That will make the SQLconnection lost between different server calls/responses and get a parse error. I did a function "getNewSqlConnection" that will copy all the settings of the TSQLConnection into a new instance.
Now the client call method looks like this:
procedure TMainForm.BtnPostOrderHistoryClick(Sender: TObject);
begin
ttask.Run(
procedure
var
ServerMethods1Client: TServerMethods1Client;
SqlConnectionLocal: TSqlConnection;
errors: Integer;
begin
// Call Server Method
SqlConnectionLocal := DMServerConnection.getNewSqlConnection(Self);
ServerMethods1Client := TServerMethods1Client.Create(SqlConnectionLocal.DBXConnection);
try
errors := ServerMethods1Client.postCustOrderHist(EditCustomerId.Text);
if errors = 0 then
TThread.Synchronize(nil,
Procedure
begin
showNotification(StrSentSuccessfully)
end)
else
TThread.Synchronize(nil,
Procedure
begin
showNotification(StrSendingFailed + '(' + errors.ToString + ' not sent)')
end);
finally
ServerMethods1Client.Free;
SqlConnectionLocal.Free;
end;
end);
end;
Related
I have an application that makes thousands of HTTP requests along the day.
For best performance, I decided to create only once the IdHTTP object and use this same object for all requests.
This is where the problem begins. While creating one IdHTTP for each request, everything went fine.
The code is pretty basic:
constructor HTTPThread.Create;
begin
inherited Create(false);
httpObject:= TIdHTTP.Create(Nil);
sslObject:= TIdSSLIOHandlerSocketOpenSSL.Create(Nil);
sslObject.SSLOptions.Method:= sslvTLSv1_2;
httpObject.IOHandler:= sslObject;
httpObject.Request.Accept:= frmHTTPRequests.Edit1.Text;
httpObject.Request.UserAgent:= frmHTTPRequests.Edit3.Text;
httpObject.ReadTimeout:= 15000;
httpObject.HandleRedirects:= true;
FreeOnTerminate:= true;
OnTerminate:= TerminateProc;
end;
procedure HTTPThread.DoRequests;
var
htmlSource: string;
begin
try
htmlSource:= httpObject.Get(Link);
//a bunch of other stuff with HTML source
except
on E : Exception do
Synchronize(procedure
begin
errorList.Add(E.Message);
errorList.SaveToFile('Error.txt');
end);
end;
end;
I created this except saving the Error.txt file to watch what is happening...
The code runs fine sometimes for the first 1k requests, sometimes for the first 2k, it varies. Suddently, it starts writing on the TXT file the same error:
Connection reset by peer. domain.com - Socket Error # 10054
I tried to disconnect the httpObject, tried httpObject.Request.Clear, nothing seems to work.
Are there any chance to make this work?
For some reasons Indy doesn't close socket when server respond with Connection reset by peer, so you need to do it manually.
procedure HTTPThread.DoRequests;
const
MAX_TRIES_COUNT = 5;
var
htmlSource: string;
TriesCount: Integer;
begin
TriesCount := 0;
repeat
try
htmlSource:= httpObject.Get(Link);
//a bunch of other stuff with HTML source
except
on E: Exception do
begin
if E.Message.Contains('by peer') then
begin
httpObject.Disconnect;
// Try to solve network connection issues
Continue;
end
else
begin
// Some other error handlers
end;
end;
inc(TriesCount);
end;
until (httpObject.ResponseCode = 200) or (TriesCount > MAX_TRIES_COUNT);
end;
P.S. I don't recommend you to use Synchronize() for threads synchronization. Try to use TCriticalSection or TMonitor instead.
I’m writing a simple Windows TCP/IP server application, which only needs to communicate with one client at a time. My application has four threads:
Main program which also handles transmission of data as needed.
Receive incoming data thread.
Listen thread to accept connection requests from the client.
A ping thread which monitors everything else, and transmits heartbeat messages as needed. I realise that the latter shouldn’t really be necessary with TCP/IP, but the client application (over which I have no control) requires this.
I’ve confirmed in task manager that my application does indeed have four threads running.
I’m using blocking TCP/IP sockets, but my understanding is that they only block the calling thread – the other threads should still be allowed to execute without being blocked. However, I have encountered the following issues:
If the ping thread deems the connection to have died, it calls closesocket(). However, this appears to be being blocked by the call to recv() in the receive thread.
The main application is unable to transmit data while the receive thread has a call to recv() in progress.
The socket is being created via the accept() function. At this stage I’m not setting any socket options.
I've now created a simple two thread program which illustrates the problem. Without the WSA_FLAG_OVERLAPPED flag, the second thread gets blocked by the first thread, even though this would appear to be contrary to what is supposed to happen. If the WSA_FLAG_OVERLAPPED flag is set, then everything works as I would expect.
PROJECT SOURCE FILE:
====================
program Blocking;
uses
Forms,
Blocking_Test in 'Blocking_Test.pas' {Form1},
Close_Test in 'Close_Test.pas';
{$R *.res}
begin
Application.Initialize;
Application.CreateForm(TForm1, Form1);
Application.Run;
end. { Blocking }
UNIT 1 SOURCE FILE:
===================
unit Blocking_Test;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, WinSock2;
type
TForm1 = class(TForm)
procedure FormShow(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
Test_Socket: TSocket;
Test_Addr: TSockAddr;
wsda: TWSADATA; { used to store info returned from WSAStartup }
implementation
{$R *.dfm}
uses
Debugger, Close_Test;
procedure TForm1.FormShow(Sender: TObject);
const
Test_Port: word = 3804;
var
Buffer: array [0..127] of byte;
Bytes_Read: integer;
begin { TForm1.FormShow }
Debug('Main thread started');
assert(WSAStartup(MAKEWORD(2,2), wsda) = 0); { WinSock load version 2.2 }
Test_Socket := WSASocket(AF_INET, SOCK_DGRAM, IPPROTO_UDP, nil, 0, 0{WSA_FLAG_OVERLAPPED});
assert(Test_Socket <> INVALID_SOCKET);
with Test_Addr do
begin
sin_family := AF_INET;
sin_port := htons(Test_Port);
sin_addr.s_addr := 0; { this will be filled in by bind }
end; { with This_PC_Address }
assert(bind(Test_Socket, #Test_Addr, SizeOf(Test_Addr)) = 0);
Close_Thread := TClose_Thread.Create(false); { start thread immediately }
Debug('B4 Rx');
Bytes_Read := recv(Test_Socket, Buffer, SizeOf(Buffer), 0);
Debug('After Rx');
end; { TForm1.FormShow }
end. { Blocking_Test }
UNIT 2 SOURCE FILE:
===================
unit Close_Test;
interface
uses
Classes;
type
TClose_Thread = class(TThread)
protected
procedure Execute; override;
end; { TClose_Thread }
var
Close_Thread: TClose_Thread;
implementation
uses
Blocking_Test, Debugger, Windows, WinSock2;
type
TThreadNameInfo = record
FType: LongWord; // must be 0x1000
FName: PChar; // pointer to name (in user address space)
FThreadID: LongWord; // thread ID (-1 indicates caller thread)
FFlags: LongWord; // reserved for future use, must be zero
end; { TThreadNameInfo }
var
ThreadNameInfo: TThreadNameInfo;
procedure TClose_Thread.Execute;
procedure SetName;
begin { SetName }
ThreadNameInfo.FType := $1000;
ThreadNameInfo.FName := 'Ping_Thread';
ThreadNameInfo.FThreadID := $FFFFFFFF;
ThreadNameInfo.FFlags := 0;
try
RaiseException( $406D1388, 0, sizeof(ThreadNameInfo) div sizeof(LongWord), #ThreadNameInfo );
except
end; { try }
end; { SetName }
begin { TClose_Thread.Execute }
Debug('Close thread started');
SetName;
sleep(10000); { wait 10 seconds }
Debug('B4 Close');
closesocket(Test_Socket);
Debug('After Close');
end; { TClose_Thread.Execute }
end. { Close_Test }
P.S. Since setting the WSA_FLAG_OVERLAPPED attribute has fixed the problem, I've posted the above for academic interest.
If the ping thread deems the connection to have died, it calls closesocket(). However, this appears to be being blocked by the call to recv() in the receive thread.
That's just a bug in your code. You cannot free a resource in one thread while another thread is, or might be, using it. You will have to arrange some sane way to ensure that you don't create race conditions around access to the socket.
To be clear, there is no way you can know what that kind of code could possibly do. For example, consider:
The thread actually hasn't called recv yet, it's about to call recv but the scheduler hasn't got around to it yet.
The other thread calls closesocket.
A thread that is part of a system library opens a new socket and happens to get the same socket descriptor you just closed.
Your thread now gets to call recv, only it's receiving on the socket the library opened!
It is your responsibility to avoid these kinds of race conditions or your code will behave unpredictably. There's no way you can know what the consequence of performing random operations on random sockets could be. So you must not release a resource in one thread while another thread is, might be, or (worst of all) might be about to be, using it.
Most likely what's actually happening is that Delphi has some kind of internal synchronization that is trying to save you from disaster by blocking the thread that can't safely make forward progress.
UPDATE: accept() creates the new socket with the same attributes as the socket used for listening. Since I hadn’t set the WSA_FLAG_OVERLAPPED attribute for the listen socket, this attribute wasn’t being set for the new socket, and options like the receive timeout didn’t do anything.
Setting the WSA_FLAG_OVERLAPPED attribute for the listen socket seems to have fixed the problem. Thus I can now use the receive timeout, and the Ping thread no longer needs to close the socket if no data has been received.
Setting the WSA_FLAG_OVERLAPPED attribute for the listen socket also seems to have addressed the blocking other threads issue.
My Delphi Berlin app uses TIdHttpServer to get some data from client via HTTP GET, process it and send it back.
All logic is performed within a single event handler: OnCommandGet. The identifier is received in a QueryString, then data will be transformed and returned back to client inside the same OnCommandGet event handler.
Data transformation is implemented in a separate thread which uses PostMessage to inform the main thread that the worker thread completes the execution and the data is ready to be sent back to client.
The data is sent in a AResponseInfo.ContentText property.
My question is:
How do I make OnCommandGet handler wait until the worker thread
does its job and sends the pointer to a transformed data, so I can get
the value and fire it back in a AResponseInfo.ContentText?
UPDATE
Here is the pseudo-code I want to execute:
type
TMyResponsesArray = array[0..5] of TMyObjectAttributes;
PMyResponsesArray = ^TMyResponsesArray;
{There will be 6 tasks run in parallel. Tasks' responses
will be stored in the below declared Responses array.}
var
Responses: TMyResponsesArray;
{Below is a Server handler, which takes the input parameter and calls
a proc which runs 6 threads in parallel. The result of each thread is
stored as an ordered array value. Only when the array is completely
populated, ServerCommandGet may send the response!}
procedure TMainForm.ServerCommandGet(AContext: TIdContext;
ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
var
ObjectId: string;
begin
ObjectId := ARequestInfo.Params.Values['oid'];
RunTasksInParallel(ObjectId);
end;
{Below is a procedure invoked by ServerCommandGet. It runs 6 tasks in
parallel. Each of the thread instantiates an object, sets its basic
parameter and fires the method. Each task runs queued. When each thread
completes the job, it sends a WM to the main thread (via ParentHandler
which must accept and process the response.}
procedure TMainForm.RunTasksInParallel(const ObjectId: string);
const
c: array[0..5] of Byte = (0, 1, 2, 3, 4, 5);
var
ParentHandle: HWND;
begin
{running 6 tasks in parallel}
TTask.Run(
procedure
begin
TParallel.For(Low(c), High(c),
procedure(index: Integer)
var
MyObj: TMyObject;
i: Byte;
begin
i := c[index];
MyObj := TMyObject.Create;
try
MyObj.SetMyParameter := Random(10);
Responses[i] := MyObj.CallMyMethd(ObjectId);
TThread.Queue(nil,
procedure
begin
SendMessage(ParentHandle,
UM_DATAPACKET, i, Integer(#Responses));
end);
finally
MyObj.Free;
end;
end);
end);
end;
{Now the WM handler. It decreases internal task counter and when
TaskCounter = 0, it means that all tasks finished execution and the
Responses array is fully populated. Then we somehow need to pass the
Response array to the ServerCommandGet and send it back to client...}
procedure TMainForm.OnDataPacket(var Msg: TMessage);
begin
i := Msg.WParam;
Responses := PMyResponsesArray(Msg.LParam)^;
{Skipped for for brevity:
When ALL tasks have finished execution, the Responses array is FULL.
Then all array values are wrapped into XML and sent back to initial
invoker ** ServerCommandGet ** which must send XML to client.}
end;
Your use of a global Responses array is not safe, unless you limit TIdHTTPServer to allow only 1 connected client at a time. Otherwise, you could potentially have multiple clients sending requests at the same time and overwriting each other's values in the array. Each invokation of ServerCommandGet() should use a local array instead.
TIdHTTPServer is not designed for the type of asynchronous processing you are attempting to do. ServerCommandGet() must block, as TIdHTTPServer sends a response to the client when the OnCommandGet handler exits, unless the handler sends a response first, which you are not doing. So, regarding your task thread management, I would suggest either:
getting rid of TTask.Run() and have RunTasksInParallel() call TParallel.For() directly.
or at least calling TTask.Wait() on the TTask object that is calling TParallel.For().
Either way will make RunTasksInParallel() block (and thus make ServerCommandGet() block) until all tasks have finished. Then you can send the response to the client immediately when RunTasksInParallel() exits. You don't need to wait for the tasks to post UM_DATAPACKET to the main thread and round-trip back into TIdHTTPServer. If you are using UM_DATAPACKET for other things, that's fine, but I do not recommend using it for your HTTP processing.
Try something more like this instead:
const
MaxResponses = 6;
type
TMyResponsesArray = array[0..MaxResponses-1] of TMyObjectAttributes;
{$POINTERMATH ON}
PMyResponsesArray = ^TMyResponsesArray;
{There will be 6 tasks run in parallel. Tasks' responses
will be stored in the below declared Responses array.}
procedure TMainForm.ServerCommandGet(AContext: TIdContext;
ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
var
ObjectId: string;
Responses: TMyResponsesArray;
begin
ObjectId := ARequestInfo.Params.Values['oid'];
RunTasksInParallel(ObjectId, #Responses);
{ALL tasks have finished execution, the Responses array is FULL.
Wrap all array values into XML and send it back to the client.}
end;
{Below is a procedure invoked by ServerCommandGet. It runs 6 tasks in
parallel. Each of the thread instantiates an object, sets its basic
parameter and fires the method.}
procedure TMainForm.RunTasksInParallel(const ObjectId: string; Responses: PMyResponsesArray);
begin
{running 6 tasks in parallel}
TParallel.For(0, MaxResponses-1,
procedure(index: Integer)
var
MyObj: TMyObject;
begin
MyObj := TMyObject.Create;
try
MyObj.SetMyParameter := Random(10);
Responses[index] := MyObj.CallMyMethd(ObjectId);
finally
MyObj.Free;
end;
end
);
end;
I would also not recommend doing the database updates in the main thread, either. If you can't update the database directly in ServerCommandGet(), or directly in the individual task threads, then I would suggest having a separate thread dedicated for database updates that you post to as needed. Stay out of the main thread as much as possible.
I'm using XE8 and I'm trying to built an example of my real world application.
I need to communicate between the main "service thread" and the OTL thread pool.
The examples are all set with forms and Monitors. I don't need those, but I can't figure out a way to write a clean code. So far this is what I did:
TProcessWorker = Class( TOmniWorker )
strict private
FTaskID : int64;
FIndex : Integer;
FFolder : String;
protected
function Initialize: Boolean; override;
public
procedure WriteTask( var msg : TMessage); message _AM_WriteTask;
End;
{ TProcessWorker }
function TProcessWorker.Initialize: Boolean;
begin
FTaskID := Task.UniqueID;
FIndex := 0;
result := True;
FFolder := Format('%s/%d', [Task.Param['Folder'].AsString, FTaskID]);
ForceDirectories(FFolder);
end;
Implemented as:
procedure TProcessWorker.WriteTask(var msg: TMessage);
var
ps : PString;
L : TStringStream;
begin
Ps:= PString(msg.LParam);
L := TStringStream.Create( ps^ );
try
L.SaveToFile( format('%s\%d.txt',[FFolder, fIndex]) );
finally
l.Free;
inc(FIndex);
end;
end;
In the main thread, to create the pool, I'm calling:
FThreadPool := CreateThreadPool('Thread pool test');
and
var
lFolder : String;
Process : IOmniWorker;
begin
lFOlder := ExtractFilePath(ParamStr(0));
Process := TProcessWorker.Create;
CreateTask( Process, 'Task test').Unobserved.SetParameter('Folder',lFolder).Schedule(FThreadPool);
I don't know how to call correctly my worker thread. In my real application, several thread will be triggered and I need to be sure I using correctly the threadpool.
1) By calling CreateTask as I am, how am I making a correct use of threadpool? It's seems odd to me to call CreateTask for every Process I need.
2) The worker thread is never triggered. How should I make my Worker thread work! :)
Regards,
Clément
OmniThreadLibrary test 08_RegisterComm shows how to communicate directly between two threads.
Basically, you have to create an instance of IOmniTwoWayChannel and register its endpoint in the worker's Initialize method with Task.RegisterComm(<channel>).
You can then send messages in a 'normal' way with <channel>.Send(<message>, <data>) and they will be dispatched to other task's message method if you decorate it in a Delphi way:
procedure MessageHandler(var msg: TOmniMessage); message <message>;
check http://otl.17slon.com/book/doku.php?id=book:howto:connectionpool
my feeling is that OTL is based upon data containers, not threads.
so I think you need to make a queue of task requests that your "main thread" would inject tasks into.
the idea of pools is that they manage themselves! you should not communicate with a specific worker thread, you should just sent work requests into it, and then let the pool spawn/kill worker threads as it sees fit.
if you need feedback from every specific thread, I'd rather include TForm.Handle or maybe the TOmniMonitor pointer into the task request record, and make the worker thread to call back and communicate with the form, no the from with the thread
I need to make a long running task in the background. I'm using OmniThreadLibrary hopping this could help me.
I use dbexpress+mssql driver. I can connect ok when is in the main thread, but get:
Project Project1.exe raised exception class TDBXError with message
'DBX Error: Driver could not be properly initialized. Client library
may be missing, not installed properly, of the wrong version, or the
driver may be missing from the system path.'.
The connections are created in each thread, not shared datamodule:
type
TdbManager = class(TObject)
private
{ private declarations }
FCon: TSQLConnection;
public
{ public declarations }
procedure Open(Driver:String; aparams:TStringList);overload;
procedure Close;
constructor Create;
destructor Destroy;override;
end;
constructor TdbManager.Create;
begin
inherited Create;
FCon := TSQLConnection.Create(nil);
end;
procedure TdbManager.Open(Driver: String; aparams: TStringList);
var
i: Integer;
key:string;
begin
FCon.DriverName := Driver;
for i := 0 to params.Count - 1 do
begin
key := params.Names[i];
FCon.Params.Values[key] := params.Values[key];
end;
LogMsg('Open DB '+ Driver + ': ' + FHost + '\' + FDatabase);
FCon.Open;
LogMsg('Done.');
end;
And the background task is executed:
procedure TBackupPlan.OnScheduleTrigger(Sender: TScheduledEvent);
begin
Parallel.Async(procedure
begin
ExecuteDataTask( Sender.Name );
end);
end;
procedure TBackupPlan.ExecuteDataTask(const Name: String);
var
db:TdbManager;
begin
db := nil;
db := TSqlServerManager.Create;
db.Open(self.Driver, options);
result := db;
end;
If I execute this directly, open ok. If I use Parallel.Async then get the error. What is happend here?
I found information about this here:
http://docwiki.embarcadero.com/RADStudio/en/DbExpress_Database_Specific_Information
MSSQL Driver Requires Calls to CoInitialize and CoUninitialize for Console Applications and Worker Threads
The MSSQL driver does not call CoInitialize or CoUninitialize. Earlier
versions of the MSSQL driver, which is a COM driver, called
CoInitialize and CoUninitialize directly, which is not a good
practice. VCL applications take care of these calls for you, so VCL
applications do not require calling CoInitialize and CoUninitialize.
However, applications using the MSSQL driver in console applications
or in worker threads need to call CoInitialize/CoUninitialize. If this
call is not made, you will see the following error message: "DBX Error: Driver could not be properly initialized. Client library may be
missing, not installed properly, of the wrong version, or the driver
may be missing from the system path." For help on CoInitialize, see
the CoInitialize Function on MSDN.