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