Search code examples
multithreadingdelphicallbackdatasnap

Delphi - Asynchronous Datasnap Method Calls


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"


Solution

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