Jump to content
araujoarthur

How to handle Asynchronous procedures in Delphi

Recommended Posts

Hello y'all!

I'm developing a Shopee Wrapper in Delphi and ran into a pitfall. I also ran into a similar problem that was answered on StackOverflow.

I need to handle two kinds of asynchronous behavior that are needed for the main program (and the VCL visual part), but I don't know WHAT I should do for this.

 

My issue starts with the following constructor noted in the code block (1). After the Authorize; call, any instruction dependent on Authorization Code falls into Undefined Behavior because there's no guarantee I have a code. I would need to wait for Authorize to return before following on. My issue arises from the fact that Authorize creates an object and starts a routine that is Asynchronous, a HTTP listener (as noted in code block) (2). The AuthorizationRequest procedure is implemented as shown and so does FieldsReadyHandler  (which is the subject of my older question) in the current version.(3, 4).

 

I was told to post here to get a better guidance on how to learn this subject and overcome this obstacle. I would like to answer the question "Why is there a server that is created then destroyed after a simple request". That was a design choice I made (maybe a poor one, I'm open to change this) for the URI. I needed it to be local so I came with the idea of opening a simple http server, waiting for the Authorization redirect, catching the code and shop_id then freeing it. This process is necessary at least once every 365 days.

 

Usually it's a matter of HOW to do that, but honestly at this point I'm thinking that I overcomplicated it and don't even know WHAT I should do to overcome this. Anyways, if I were to rewrite I can see my self falling in the same spot where my subsequent tasks depends on the async task to finish. I also tried to add an event to inform the Context that it already obtained the code but (my shot is) it raises access violation because of the way the object is released.

Sorry I can't synthesize a TL;DR for this one, but if I had to index my questions the output would be: What is the best approach to this type of problem and how is it handled in pascal? In JS, for example, I would chain infinite .then() until the routine doesn't need the async part anymore. I also feel it is going to become a frequent pain if I don't learn to handle it now as requests are asynchronous.

 

Thanks in advance.

{1. The constructor }

constructor TShopeeContext.Create(AMode, AAPIKey, APartnerID: string);
begin

  if (AMode <> TEST_SHOPEE) and (AMode <> PRODUCTION_SHOPEE_BR) then
    raise Exception.Create('Mode Invalid for Context');

  FHost := AMode;
  FAPI_Key := AAPIKey;
  FPartnerID := APartnerID;

  UpdateInternalConfigFields;

  if FRefreshToken = '' then
    FHasRefreshToken := False
  else
    FHasRefreshToken := True;

  // Checks if the authorization has been granted and is younger than 365 days.
  if (not FAuthorizationGranted) or ((Now - FAuthorizationDate) > 365) then
  begin
    Authorize;
  end;

end;

{ 2. Authorize Procedure }

procedure TShopeeContext.Authorize;
var
  Authorizator: TShopeeAuthorizator;
begin
  // Request Authorization;
  Authorizator := TShopeeAuthorizator.Create(FHost, FAPI_Key, FPartnerID, 8342);
  try
    Authorizator.AuthorizationRequest;
  finally
    Authorizator.Free;
  end;

end;

{ 3. AuthorizationRequest implementation }

procedure TShopeeAuthorizator.AuthorizationRequest;
begin
  // Obtem a assinatura da chamada
  FSignature := GetPublicSignString(API_PATH_AUTHORIZATION, FTimestamp);

  // Constroi os parametros
  FRequestParameters.AddItem('partner_id', FPartnerID);
  FRequestParameters.AddItem('timestamp', FTimeStamp);
  FRequestParameters.AddItem('sign', FSignature);
  FRequestParameters.AddItem('redirect', REDIRECT_URL_WS+':'+IntToStr(FPort));

  ShellExecute(0, 'open', PChar(GenerateRequestURL(API_PATH_AUTHORIZATION, FRequestParameters)), nil, nil, SW_SHOWNORMAL);

  FCatcherServer := TCatcherServer.Create();
  FCatcherServer.OnFieldsReady := FieldsReadyHandler;
  FCatcherServer.Listen(FPort);
end;

{4. FieldsReadyHandler implementation}

procedure TShopeeAuthorizator.FieldsReadyHandler(Sender: TObject; Code,
  AuthorizatedID: string);
begin
  // Handle Code, Auth Type and AuthorizatedID.
  FConfigurator := TConfiguratorFile.Create;
  try
    FConfigurator.SaveAuthorizationInfo(Code, AuthorizatedID, (Sender as TCatcherServer).AuthorizationType);
    FSuccess := True;
  finally
    FConfigurator.Free;
  end;

  TThread.Queue(nil, procedure
  begin
    Sender.Free;
  end);
end;

Share this post


Link to post
Posted (edited)

I don't completely understand your problem but I'm assuming the issue is the following. You're running something that has to be done on the background and you don't want your application to freeze up / be unresponsive until its done. Or you want to wait until a specific thing is done.

 

 https://docwiki.embarcadero.com/RADStudio/Athens/en/Using_TTask_from_the_Parallel_Programming_Library

TTask.Run(procedure
begin

// Whatever async thing you want to run

end);

Let's say now you want to wait until its done, or check at least.

 

var aTask : ITask;

aTask := TTask.Run(procedure
begin

// Whatever async thing you want to run

end);

aTask.Wait();

What's important is that when you do something on VCL you do it on the main thread. The TTask.Run is always on a separate thread from the main. You'll have to use https://docwiki.embarcadero.com/Libraries/Alexandria/en/System.Classes.TThread.Synchronize to call the main thread and do VCL things from an async thread.

 

aTask.Wait might achieve the opposite of what you want: Locking the main thread. 

If for some reason you have to use it elsewhere there's also a TTaskStatus that you could check. Or you can make another TTask.Run and put aTask.Wait inside it.

 

Hope this helps!
 

Edited by mitch.terpak

Share this post


Link to post

It was my comment about the server creation which seemed unnecessary. This was a wrong assumption, and the server is needed for the authorization process.

 

However, there are still some things in your code that could be improved. 

 

I will start with simple things. 

 

You are have plenty of unnecessary nil checks. If you want to return a nil or some field instance if it is assigned, then you can just return the value in the field as if the field is not assigned its default value will be nil. (Note, when it comes to local variables, they must be explicitly initialized to nil, as their default value will be random, unlike for object fields)

 

So following code can be replaced

function TServer.ServerLog: TStringList;
begin
  Result := Nil;
  if Assigned(FServerLog) then
  begin
    Result := FServerLog;
  end;
end;

with

 

function TServer.ServerLog: TStringList;
begin
  Result := FServerLog;
end;

Next, you are constructing FServerLog in the constructor, so the whole nil check is pointless anyway because during the server instance lifetime it will be valid, assigned instance. In cases where it would not be a valid instance. If you would have a case where some function could return nil instance, you would have to always check for nil, before using such object.

 

Free method can be safely called on nil instances, so you don't have to check if instance is assigned before calling Free. Following would be fine:

 

destructor TServer.Destroy;
begin
  FServer.Free;
  FServerLog.Free;
  inherited;
end;

Because server uses server log, I would also reverse order of construction and destruction and create server log first, and then the server, also server would be destroyed first, and then server log.

 

You have similar code in other places and it can be simplified, too.

 

Now, back to the actual problem you have been asking. 

 

First of all, TShopeeAuthorizator is inherently connected with TCatcherServer. It would make sense to create server in TShopeeAuthorizator constructor and destroy it in its destructor. This would also prevent memory leak you have if the FieldsReadyHandler is never called.

 

This would also remove rather ugly Sender.Free code. It is not that it cannot be used in that way, but commonly such code can be replaced with better constructs. 

 

Next it would be better to move parts of the TShopeeContext.Authorize inside the TShopeeAuthorizator. This would also remove the need to have global AuhorizationDone event. Make it a field in TShopeeAuthorizator and create it in its constructor, and destroy in the destructor.

 

The whole waiting for even loop will then be moved inside TShopeeAuthorizator.AuthorizationRequest which would be converted to function returning Boolean to detect successful authorization.

 

procedure TShopeeContext.Authorize;
var
  Authorizator: TShopeeAuthorizator;
begin
  // Request Authorization;
  Authorizator := TShopeeAuthorizator.Create(FDataHolder, FHost, FAPI_Key, FPartnerID, 8342);
  try
    if Authorizator.AuthorizationRequest(45000) then
    begin
      ShowMessage('Autenticado com Sucesso');
      ShowMessage(FDataHolder.Code + ' ' + FDataHolder.EntityID);
    end
    else
      raise Exception.Create('Authorization Timed Out');
  finally
    Authorizator.Free;
  end;
end;

Additionally, you can also make TShopeeContext.Auhorize a Boolean function that would merely return the value returned by AuthorizationRequest. This would also move error handling to the higher level code which can then have better control of how to handle success and failure.

 

You can also add event handlers FOnSuccess and FOnFailure to TShopeeContext so the outside code can hook to them. In such case Authorize does not need to be a function (but you can still leave it like that, so your code would have more flexibility.

 

  TShopeeContext = class
  private
    FOnAuhorizeSucess, FOnAuthorizeFailure: TNotifyEvent;
  ...
  published
    property OnAuthorizeSucess: TNotifyEvent read FOnAuhorizeSucess write FOnAuhorizeSucess;
    property OnAuthorizeFailure: TNotifyEvent read FOnAuthorizeFailure write FOnAuthorizeFailure;
  end;

function TShopeeContext.Authorize: Boolean;
var
  Authorizator: TShopeeAuthorizator;
begin
  Authorizator := TShopeeAuthorizator.Create(FDataHolder, FHost, FAPI_Key, FPartnerID, 8342);
  try
    Result := Authorizator.AuthorizationRequest(45000);
    if Result then
      begin
        if Assigned(FOnAuthorizeSucess) then FOnAuthorizeSucess(Self);
      end
    else
      begin
        if Assigned(FOnAuthorizeFailure) then FOnAuthorizeFailure(Self);
      end;
  finally
    Authorizator.Free;
  end;
end;

Of course, you can move both success and failure into single event handler, but then you would need to define handler that would have additional parameter.

 

Share this post


Link to post

I cannot tell what your code is doing for sure. However, your question is also general about how to tackle asynchronous programming in Object Pascal. You are calling procedures in your class that do not return any values, where a more functional approach might be better suited for your needs.

 

Assuming the consumer is in the main thread that you do not want to lock, you have a couple of options:

 

You could use a TThread descendent class and set its OnTerminate even to a TNotifyEvent in your main thread (i.e., a method with the procedure(Sender: TObject) signature) that will receive the TThread descendent before its destruction and allow you to extract the necessary data.

 

Alternatively, you could use an IFuture<T> (System.Threading) and query its value when you are ready, but this can be a blocking call if your future result is not yet available.

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×