Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Delphi Threading With TRestRequest

I am relatively new to the RestRequest tools. I am hoping somebody has solved the issue of waiting for the async call to finish...

I have a program which makes hundreds of different api calls, the call then processes the result, and passes the result back to the procedure which called the api's execution, which can then continue...

Only problem as always with doing such calls without threading, is that the software hangs until the call is complete... To try and remedy this I changed RESTRequest.Execute; to RESTRequest.ExecuteAsync();, but now my problem is that my code continues without waiting for the restrequest's response.

again to try and bypass this problem I have tried several solutions, even api.RESTRequest.ExecuteAsync().WaitFor; (which throws an error thread error: the handler is invalid (6))

Is there any way at all that I can change the below function to run as a separate thread (only the execute part is important to run in thread)... Basically I just want to show an animated loading icon everytime the function is called, and for the rest of my code to wat until this function completes...

I hope there is a simpler solution than to start using full on multithreading..

Code

function run_api_command():boolean;
begin

  result := false;
  RESTResponse.Content.Empty;
  RESTAdapter.Dataset:= ds_action;
  RESTAdapter.RootElement:= '';

  try
    RESTRequest.ExecuteAsync;

    if(ds_action.Active = false) then ds_action.Active:=true;
    if(ds_action.FieldByName('result').AsString<>'Success') then
      begin
        showmessage(ds_action.FieldByName('result').AsString);
      end
    else
      begin
        RESTAdapter.RootElement:= 'data';
        result := true;
      end;
  except
    on E: Exception do
      begin
        if(e.Message = 'REST request failed: Error sending data: (12007) The server name or address could not be resolved') then
          begin
            if(messagedlg('Could not connect to server. Would you like to retry?', mterror,[mbYes,mbNo],0)=mrYes) then
              begin
                result := run_api_command();
              end;
          end
        else
          begin
            showmessage(RESTResponse.Content);
          end;
      end;
  end;
end;
like image 918
Marcel Avatar asked Sep 05 '17 21:09

Marcel


2 Answers

ExecuteAsync() runs in a worker thread (the TRESTExecutionThread object that it returns). However, ExecuteAsync() has an AFreeThread parameter that defaults to True. As the documentation clearly states:

When the AFreeThread parameter is set to False, this method returns a reference to this execution thread.

Note: If the AFreeThread parameter is set to True, the method returns an invalid reference.

So calling WaitFor() on the returned object pointer will crash by default.

Even if the returned object pointer were valid when AFreeThread=True, calling WaitFor() would still crash. When a TThread object's FreeOnTerminate property is set to True, the object frees its underlying API handle when the thread terminates, which causes WaitFor() to fail with the "handle is invalid" error. TThread.WaitFor() has a logic bug 1 that does not handle the case when TThread.FreeOnTerminate=True.

(1 That bug was introduced all the way back in Delphi 6, when TThread was rewritten for Kylix support, and it was never corrected in later versions.)

If you want the caller to wait for a response from the REST server, either:

  • use Execute() instead of ExecuteAsync(), or at least set AFreeThread=False so you can call WaitFor() (or equivalent, like MsgWaitForMultipleObjects()) on the thread object, and then deal with the consequences of performing a wait in the main UI thread:

  • use Execute(), but move your entire logic to your own worker thread, and have it sync with the main UI thread as needed.

The better solution is to simply not wait at all, which means redesigning your code flow. Continue using ExecuteAsync(), but pass it a completion callback that will be called when the request is complete, and let that callback drive the next step in your code. Don't actively wait for ExecuteAsync to complete, let it notify you.

procedure run_api_command();
begin
  ...
  RESTRequest.ExecuteAsync(
    procedure
    begin
      if not RESTResponse.Status.Success then
      begin
        // do something ...
      end else begin
        // do something else ...
      end;
    end,
    True
  );
end;
like image 157
Remy Lebeau Avatar answered Oct 02 '22 12:10

Remy Lebeau


Extending onto Remy's answer...

"but now my problem is that my code continues without waiting for the restrequest's response"

This is the exact purpose of Async requests. You're not supposed to wait, you're supposed to send the request and move on to something else. You're also supposed to provide a callback procedure if you wish to capture the result, but I don't see you doing that in your code.

Synchronous and Asynchronous calls are quite different design architectures which cannot be simply switched between as trivially as you may wish. You can't just change Execute to ExecuteAsync without also re-designing the rest. Execute by nature waits for the response within the calling thread. However, ExecuteAsync by nature spawns the request in a new thread, so that your code can continue while it's doing your work in the background. There is no waiting involved in Async requests - it defies the whole purpose of them.

The most ideal option in your case is to chain one request off of the other's response. Meaning, send your first request only. Once you receive the response, then send your next request from within its response callback.

"...since I want to create the function as a sort of "global api call" function to make coding a bit less..."

That's perfectly fine. You can still have one single main procedure which performs all the work. It's when to make each of these calls which matters here. A button's OnClick event is never the appropriate place to do such a thing, though. The only thing that button should do (since it's in the main UI thread) is initiate a request. If you want your UI to be responsive at all after this point, then that request should spawn a new thread in some way, shape, or form.

In any case though, WaitFor just won't cut it for your request. You want a responsive UI. WaitFor will defy this requirement, and lock up your application's main thread anyway. The whole purpose of doing this work in another thread is to show a "waiting" animation, which will not be animated if your main thread is busy waiting for anything.


EDIT

I actually thought of another possibility. Still spawn your first request only. But instead of "chaining" one request off of the other's response, just do all the rest of the work from within that first request's callback. Since at this point, it's already in a thread, there's not necessarily a need to spawn yet another thread for the next request. Just perform the remainder of what you need after the first request, in the existing thread you've already spawned.


On a side note, this is a very common problem in the modern programming world...

I Hate Programming

like image 39
Jerry Dodge Avatar answered Oct 02 '22 14:10

Jerry Dodge