Inspirel banner

Programming Distributed Systems with YAMI4

9.2.1 Ada

The Ada client and server can be compiled by running make, assuming that the core library was already compiled.

The client program is entirely implemented in the client.adb file, which is dissected in detail below.

First the relevant YAMI4 packages are withed:

with YAMI.Agents;
with YAMI.Outgoing_Messages;
with YAMI.Parameters;

The above packages are the most commonly used client-side packages that allow the program to create the agent and to send outgoing messages with nontrivial payloads.

Some standard packages are needed to process command line arguments, handle exceptions and to report the current activity on the console:

with Ada.Command_Line;
with Ada.Exceptions;
with Ada.Text_IO;

The whole client program is implemented in a single Client procedure, which initial actions focus on argument retrieval and parsing - in addition to the requested server target, two integer numbers are read:

procedure Client is
   A : YAMI.Parameters.YAMI_Integer;
   B : YAMI.Parameters.YAMI_Integer;
begin
   if Ada.Command_Line.Argument_Count /= 3 then
      Ada.Text_IO.Put_Line
        ("expecting three parameters: " &
           "server destination and two integers");
      Ada.Command_Line.Set_Exit_Status
        (Ada.Command_Line.Failure);
      return;
   end if;

   begin
      A := YAMI.Parameters.YAMI_Integer'Value
        (Ada.Command_Line.Argument (2));
      B := YAMI.Parameters.YAMI_Integer'Value
        (Ada.Command_Line.Argument (3));
   exception
      when Constraint_Error =>
         Ada.Text_IO.Put_Line
           ("cannot parse the second or third parameter");
         Ada.Command_Line.Set_Exit_Status
           (Ada.Command_Line.Failure);
         return;
   end;

   declare
      Server_Address : constant String :=
        Ada.Command_Line.Argument (1);

In order to send the message three entities are needed at the client side: agent to handle the communication, parameters object to carry the payload (the two integer values) and the outgoing message object for progress tracking:

      Client_Agent : YAMI.Agents.Agent :=
        YAMI.Agents.Make_Agent;

      Params : YAMI.Parameters.Parameters_Collection :=
        YAMI.Parameters.Make_Parameters;

      Message :
        aliased YAMI.Outgoing_Messages.Outgoing_Message;
      State : YAMI.Outgoing_Messages.Message_State;

There is also a local procedure that is used to process the content of the reply that will be received from the server. This procedure extracts four values from the response content, which correspond to the results of four basic calculations performed by the server.

An important feature of the communication ``protocol'' in this example is that one of the components in the reply content is optional - that is, it might or might not be included in the content. The processing procedure takes care of that possibility with appropriate logic.

      procedure Process_Reply
        (Content : in out YAMI.Parameters.Parameters_Collection)
      is
         Sum : constant YAMI.Parameters.YAMI_Integer :=
           Content.Get_Integer ("sum");
         Difference :
           constant YAMI.Parameters.YAMI_Integer :=
           Content.Get_Integer ("difference");
         Product : constant YAMI.Parameters.YAMI_Integer :=
           Content.Get_Integer ("product");
         Ratio : YAMI.Parameters.YAMI_Integer;
         Ratio_Entry : YAMI.Parameters.Parameter_Entry;
         Ratio_Defined : Boolean;
      begin
         Content.Find
           ("ratio", Ratio_Entry, Ratio_Defined);
         if Ratio_Defined then
            Ratio :=
              YAMI.Parameters.Get_Integer (Ratio_Entry);
         end if;

Above, the Ratio_Defined variable is True when the reply content has the entry for the result of division (and then the Ratio variable will get that value), and False otherwise.

The processing procedure prints all received values on the console:

         Ada.Text_IO.Put_Line
           ("sum        = " &
              YAMI.Parameters.YAMI_Integer'Image (Sum));
         Ada.Text_IO.Put_Line
           ("difference = " &
              YAMI.Parameters.YAMI_Integer'Image
              (Difference));
         Ada.Text_IO.Put_Line
           ("product    = " &
              YAMI.Parameters.YAMI_Integer'Image
              (Product));
         Ada.Text_IO.Put ("ratio      = ");
         if Ratio_Defined then
            Ada.Text_IO.Put_Line
              (YAMI.Parameters.YAMI_Integer'Image (Ratio));
         else
            Ada.Text_IO.Put_Line ("<undefined>");
         end if;
      end Process_Reply;

The main part of the client program composes and sends the outgoing message.

First, the message payload is formed to contain two parameters from the command-line:

      use type YAMI.Outgoing_Messages.Message_State;
   begin

      Params.Set_Integer ("a", A);
      Params.Set_Integer ("b", B);

Then, the message is physically sent:

      Client_Agent.Send
        (Server_Address, "calculator", "calculate", Params,
         Message'Unchecked_Access);

Above, the Send operation is invoked with the following parameters:

It is important to note that the actual message is transmitted to the server in background and there is no provision for it to be already transmitted when the Send operation returns. In fact, the user task might continue its work while the message is being processed by the background I/O task.

In this particular example, the client program does not have anything to do except waiting for the response:

      Message.Wait_For_Completion;

Depending on the message state, client either processes the result with the previously defined local procedure or prints appropriate report on the console:

      State := Message.State;
      if State = YAMI.Outgoing_Messages.Replied then

         Message.Process_Reply_Content
           (Process_Reply'Access);

      elsif State = YAMI.Outgoing_Messages.Rejected then
         Ada.Text_IO.Put_Line
           ("The message has been rejected: " &
              Message.Exception_Message);
      else
         Ada.Text_IO.Put_Line
           ("The message has been abandoned.");
      end if;

Client code finishes with basic error handling:

   end;
exception
   when E : others =>
      Ada.Text_IO.Put_Line
        (Ada.Exceptions.Exception_Message (E));
end Client;

The server program is entirely implemented in the server.adb file.

First the relevant server-side YAMI4 packages are withed:

with YAMI.Agents;
with YAMI.Incoming_Messages;
with YAMI.Parameters;

The YAMI.Incoming_Messages package defines the API allowing to interact with messages sent by clients.

Some standard packages will be needed as well:

with Ada.Command_Line;
with Ada.Exceptions;
with Ada.Text_IO;

The server is implemented in the Server procedure.

One of the most important server-side entities is a message handler, which has to be a user-provided instance of the type that implements the Message_Handler interface. This interface has only one primitive operation that has to be overridden and redefined by the server code:

procedure Server is

   type Incoming_Message_Handler is
     new YAMI.Incoming_Messages.Message_Handler
     with null record;

   overriding
   procedure Call
     (H : in out Incoming_Message_Handler;
      Message : in out
      YAMI.Incoming_Messages.Incoming_Message'Class) is

The message handler in this example uses a local procedure to process the content of incoming message. This local procedure is similar in its form to the one used in the printer example.

First, appropriate data values (two integers named ``a'' and ``b'') are extracted from the content:

     procedure Process
       (Content : in out YAMI.Parameters.Parameters_Collection)
     is
        A : YAMI.Parameters.YAMI_Integer;
        B : YAMI.Parameters.YAMI_Integer;

        Reply_Params :
          YAMI.Parameters.Parameters_Collection :=
          YAMI.Parameters.Make_Parameters;

        use type YAMI.Parameters.YAMI_Integer;
     begin
        --  extract the parameters for calculations

        A := Content.Get_Integer ("a");
        B := Content.Get_Integer ("b");

Then the reply content is prepared with results of the four basic computations. As was already explained with the client code, the ``protocol'' for request-response interaction in this example is that one of the entries in the result is optional - that is, it is present or not depending on message processing.

In this particular case the result of division is included in the reply content only when it was possible to compute it - if the divisor is zero, the result cannot be computed and the server expresses that fact by not including the ratio in the reply content.

        --  prepare the answer
        --  with results of four calculations

        Reply_Params.Set_Integer ("sum", A + B);
        Reply_Params.Set_Integer ("difference", A - B);
        Reply_Params.Set_Integer ("product", A * B);

        --  if the ratio cannot be computed,
        --  it is not included in the response
        --  the client will interpret that fact properly
        if B /= 0 then
           Reply_Params.Set_Integer ("ratio", A / B);
        end if;

Once the reply content is prepared, the message can be replied to:

        Message.Reply (Reply_Params);

The above operation can be performed only once on the given incoming message, which reflects the function-call analogies of remote invocations. As a result of the above, the reply is put into the outgoing queue of the relevant channel (the one on which the originating incoming message was received) where it is immediately ready for transmission. The I/O activity can take place in background, so that the main server code is not tied during this process.

The processing procedure finishes with a report on the console.

        Ada.Text_IO.Put_Line
          ("got message with parameters " &
             YAMI.Parameters.YAMI_Integer'Image (A) &
             " and " &
             YAMI.Parameters.YAMI_Integer'Image (B) &
             ", response has been sent back");
     end Process;

The message handler's Call operation only delegates to the processing local procedure without performing any other action. After the processing procedure is done with its work (that includes preparing and posting the reply), the message handler can complete as well - this is completely independent on when exactly the reply might be actually transmitted to the client.

   begin
      Message.Process_Content (Process'Access);
   end Call;

The message handler has to be instantiated before it is given to the agent. The handler is instantiated as an aliased object so that the agent can refer to it by by its base interface's access value.

   My_Handler : aliased Incoming_Message_Handler;

The main part of the server program deals with command-line arguments, where the server target is expected.

begin
   if Ada.Command_Line.Argument_Count /= 1 then
      Ada.Text_IO.Put_Line
        ("expecting one parameter: server destination");
      Ada.Command_Line.Set_Exit_Status
        (Ada.Command_Line.Failure);
      return;
   end if;

   declare
      Server_Address : constant String :=
        Ada.Command_Line.Argument (1);

Once the server target is known, it is passed to the freshly constructed agent in order to add a new listener. The resolved target that results from this operation is printed on the console.

      Server_Agent : YAMI.Agents.Agent :=
        YAMI.Agents.Make_Agent;
      Resolved_Server_Address :
        String (1 .. YAMI.Agents.Max_Target_Length);
      Resolved_Server_Address_Last : Natural;
   begin
      Server_Agent.Add_Listener
        (Server_Address,
         Resolved_Server_Address,
         Resolved_Server_Address_Last);

      Ada.Text_IO.Put_Line
        ("The server is listening on " &
           Resolved_Server_Address
           (1 .. Resolved_Server_Address_Last));

The previously defined message handler is registered as an implementation of the logical object named ``calculator''. This object name has to be known by clients so that they can properly send their messages.

      Server_Agent.Register_Object
        ("calculator", My_Handler'Unchecked_Access);

Above, the access value of the message handler is obtained as unchecked, because the handler is defined in the deeper scope than that of the access type definition. This construct is safe, because the handler will anyway live longer than the agent and therefore there is no risk of referring to it after its finalization. In more elaborate servers the message handler might be defined at the library level, in which case the unchecked access would not be needed.

The server's environment task effectively blocks forever by falling into an infinite loop. From that time all of the server's activity is driven by incoming messages:

      loop
         delay 10.0;
      end loop;

The code concludes with rudimentary exception handling:

   end;
exception
   when E : others =>
      Ada.Text_IO.Put_Line
        (Ada.Exceptions.Exception_Message (E));
end Server;