Handling long running, blocking functions

Viewed 172

In a project I have to use an imported C function that sends a request to a server and receives a response. It is possible to provide a timeout parameter to this function to specify the maximum time before the blocking function returns if the server is not reachable.

Unfortunately this timeout is not always reliable and often the maxium timeout value is exceeded. What is the best Ada approach to handle this kind of problem?

After reading this chapter I am not sure if I could use this kind of programming pattern? The imported C function is not thread safe so only one request at a time is possible. A perfect solution would allow one to send a request (with a valid response or the timeout response normally returned by the imported C function) and a second function could be used to query if the last request timed out. Until the imported C function call has returned further requests should always return the timout response.

I am not sure what happens to the entries (programming statements inside the entries) Password_Server.Set and Process_Data.Output call if the delay alternative is chosen?

2 Answers

If the entry call is queued or waiting for access to a protected object (PO), then the entry call is aborted when the delay expires, the delay branch is executed, and the select finishes. If the entry call has been accepted (task) or is executing (PO) when the delay expires, it continues until it finishes or is requeued. If it finishes, then the entry branch is executed and the select finishes. If it is requeued with abort it behaves just as it does initially. If it is requeued without abort, then the delay branch is ignored. (Note that if it's requeued without abort, it can still be later requeued with abort, so things can be more complicated. It's best to try to avoid such a design.)

IIUC correctly, you want only one task at a time to be able to call the C operation. If one task is calling it and another tries to, the second task should be told the call timed out. Ada should abort the C operation if it takes longer than its timeout.

The only way I can see to abort the call to C is with an Asynchronous Transfer of Control. Whether that will actually do what is wanted is an open question.

This leads me to something like

package Calling_C is
   type Data is ...

   procedure Call_C (Info : in Data; Timeout : in Duration; Timed_Out : out Boolean);
end Calling_C;

package body Calling_C is
   task Control is
      entry Call_C (Info : in Data; Timeout : in Duration; Timed_Out : out Boolean);
   end Control;

   task body Control is
   begin
      Forever : loop
         select
            accept Call_C (Info : in Data; Timeout : in Duration; Timed_Out : out Boolean) do
               select
                  delay Timeout;

                  Timed_Out := True;
               then abort
                  -- Call the C operation
               end select;
            end Call_C;
         or
            terminate;
         end select;
      end loop Forever;
   end Control;

   procedure Call_C (Info : in Data; Timeout : in Duration; Timed_Out : out Boolean) is
   begin
      select
         Control.Call_C (Info => Info, Timeout => Timeout, Timed_Out => Timed_Out);
      else
         Timed_Out := True;
      end Select;
   end Call_C;
end Calling_C;

The first task gets in and calls the C operation. Subsequent tasks are not accepted immediately and return with Timed_Out set to True. If the call to C doesn't return in time, maybe it's aborted.

I don't actually know Ada very good just yet. Posting my idea here so pros can tell me whether it's a bad idea. The server would look something like this:

task Server is
  entry Request (in Input : Input_Type);
  entry Response (out Output : Output_Type);
end Server;
task body Server is
  Local_Input : Input_Type;
  Local_Output : Output_Type;
begin
  accept Request (Input : in Input_Type) do
    Local_Input := Input;
  end Request;

  Local_Output := My_C_Code(Local_Input);

  accept Response(Output : out Output_Type) do
    Output := Local_Output;
  end Response;
end Server;

How to use it:

Server.Request(Input);
select
  Server.Response(Output);
    -- got your response
  or
    delay Timeout;
    -- response timeout
  end select;

As you may have realized the Server will block on Response until the Request caller (or anyone else) decides to call it. My best guess is either a) adding select ... or terminate, or b) handle the timeout and go back to waiting for Response, sync or async.

Related