Inspirel banner

Programming Distributed Systems with YAMI4

9.1.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.Parameters;

The YAMI.Agents package is needed so that the client program can create its own agent that will manage the communication. The YAMI.Parameters package allows to build a proper message content, where the string value will be placed.

Some standard packages are also essential to manage program arguments, handle exceptions and read text from the standard input:

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

The whole client program is implemented within a single main procedure named Client. Initial actions in this procedure check whether there is one required argument describing server destination address:

procedure Client is
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);

After the program arguments are processed, client can create its own YAMI agent.

The newly created agent has to be initialized with proper constructor call:

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

The whole logic of the client program is devoted to reading lines of text from standard input:

   begin
      --  read lines of text from standard input
      --  and post each one for transmission

      while not Ada.Text_IO.End_Of_File loop
         declare
            Input_Line : constant String :=
              Ada.Text_IO.Get_Line;

For each line of text, a new message is sent to the server with parameters object that contains the given text. The parameters object also needs to be created with initialization by dedicated constructor function:

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

Once the parameters object is created, it can be filled with data - here, only one data entry is needed:

         begin
            --  the "content" field name is arbitrary,
            --  but needs to be recognized at the server side

            Params.Set_String ("content", Input_Line);

The data entry is named "content", but the name does not matter as long as it is properly recognized by the server. In other words, the choice of entry names forms a kind of contract between client and server and can be compared to the naming of method parameters in object-oriented programming.

Everything is now ready for sending the message:

            Client_Agent.Send_One_Way
              (Server_Address, "printer", "print", Params);

The message is sent as one-way, which means that there is no feedback on its progress.

The arguments to the Send_One_Way operation are:

It should be noted that the message is being sent in background, so the user task can immediately continue and read another line of text.

The program code concludes with rudimentary exception handling:

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

Another version of the client program (client_synchronous.adb) synchronizes each message separately and allows to guarantee that all lines of text are really delivered to the server, so that when the client program finishes there are no messages pending in its outgoing queue.

The code of that alternative client is very similar and there are only two differences.

The first is that yet another YAMI4 package needs to be withed in order to provide access to the outgoing message API:

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

Then the program is implemented as before:

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

procedure Client_Synchronous is
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);

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

   begin
      --  read lines of text from standard input
      --  and post each one for transmission

      while not Ada.Text_IO.End_Of_File loop
         declare
            Input_Line : constant String :=
              Ada.Text_IO.Get_Line;

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

Another implementation difference is the part that sends the message - instead of posting a one-way message to the queue, a regular message object is created and later used for synchronization with its delivery.

The message object needs to be initialized with appropriate constructor call:

            Message :
              aliased YAMI.Outgoing_Messages.Outgoing_Message;
         begin
            --  the "content" field name is arbitrary,
            --  but needs to be recognized at the server side

            Params.Set_String ("content", Input_Line);

In order to send this message, its access value needs to be given to the agent, so that further activity and message progress can be reflected in the internal state of the message object:

            Client_Agent.Send
              (Server_Address, "printer", "print", Params,
               Message'Unchecked_Access);

The user code can then synchronize itself with the message delivery by waiting on its transmission condition:

            Message.Wait_For_Transmission;

The above call ensures that the user code will not proceed until the line of text is sent to the server - if this was the last line, the program can finish immediately by exiting the main loop without any risk of losing pending messages.

Program code concludes with simple exception handling:

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

The server program (server.adb) is also implemented in a single file and begins with appropriate context clause:

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

The YAMI.Incoming_Messages package allows the server program to use the operations of incoming message objects and is almost always used by server programs.

Some standard packages are also needed:

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

procedure Server is

An important part of every server is the definition of its message handlers. In this example there is only one such handler:

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

The above type defines new tagged message handler type without any extension part - more elaborated servers with stateful message handlers will use the extension to define their state components.

The message handler type needs to implement a single Call procedure that accepts the incoming message object:

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

     procedure Process
       (Content : in out YAMI.Parameters.Parameters_Collection)
     is
     begin
        --  extract the content field
        --  and print it on standard output

        Ada.Text_IO.Put_Line
          (Content.Get_String ("content"));
     end Process;

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

In this example the Call procedure has to print the line of text that was sent by remote client on the screen - here a local Process procedure is used to inspect the message content (this is similar to the idioms of standard containers). No other processing is performed and in particular no feedback - neither reply nor rejection - is sent back to the client.

The message handler has to be instantiated in order to be used:

   My_Handler : aliased Incoming_Message_Handler;

In its main part the server program first verifies whether it was given a single required argument that is interpreted as a server target:

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

Then the agent object is created and a listener is added to it with the given target name:

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

The resolved server address is printed on the screen to show how it was expanded and to allow the user to pass it properly to the client program:

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

Then the ``printer'' object is registered with the already defined message handler:

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

After creating the listener and registering the object the server is ready for operation - every message that will arrive from remote clients will be routed to the registered message handler and the line of text that was sent by client will be printed on the screen.

In order to allow the agent to operate in background the environment task effectively stops by executing infinite loop. Such a loop is typical for servers that are entirely driven by remote messages:

      loop
         delay 10.0;
      end loop;

The server code finishes with simple exception handling:

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