The Alba Technology Demonstrator

Chip Richards

Version 1.1

This code is released under the terms of the GNU General Public License, version 2.


Table of Contents

1. Background
2. Building Alba
2.1. The Official Way
2.2. The Easy Way
3. Running Alba
3.1. Executing Alba
3.2. Alba Command Set
4. The Alba Program Code
4.1. Design Rationale
4.2. Alba's Task Structure
4.3. The Input Task
4.4. The Output Task
4.5. The Command Task
4.6. The Ping Task
4.7. The File Task
4.8. The Database Task
4.9. The Main Procedure

1. Background

Alba seems an appropriate name for this design trial for the #Ada IRC channel's info-bot. Allegra was Ada's bastard half-sister, who died in childhood after being exiled to a convent by her famous father. Her mother named her Alba, but Lord Byron overruled that choice and named her Allegra instead. Similarly, alba is allegra, in a sense, though without her full personality.

We had three reasons for writing Alba: To verify that Allegra's tasking structure is sound, to exercise the LP system with something more than a trivial example, and to provide examples to interested parties of both of these points.

2. Building Alba

Alba has been verified to work correctly under Linux 2.4.22, using the GNAT that comes as part of GCC 3.3.2. It demands nothing unusual from its platform, however, and should build and run on a wide variety of platforms.

There are actually two “ways” to build Alba from source: the official way, and the easy way.

2.1. The Official Way

Since Alba is a literate program, the official program source is an XML document, which contains both the Ada source code and its documentation. To build a usable document file, you must “weave” the XML source into your desired viewing format. To build an executable, you must “tangle” the XML source into Ada source files, then compile those sources. The official distribution package includes the XML source files and a makefile to perform the above steps.

The “extra” requirements for building Alba, beyond those commonly found on a Unix or comparable system set up for Ada development, are these:

  • LitProg - Norman Walsh's XML literate programming toolset. It includes stylesheets which permit an XSLT processor to perform the tangle and weave operations on the XML source, to produce the actual source and documentation.
  • xsltproc - Part of the GNOME Project, used by the makefile to apply the LitProg tangle and weave stylesheets to the XML source, transforming them into documentation and source. Any conforming XSLT stylesheet processor should work.

The makefile also uses a short perl script, called xweb-include, which is included in the distribution package.

2.1.1. Building the Documentation

The makefile produces the documentation in HTML format, using the weave stylesheet with xsltproc. Different stylesheets could be used to produce the document in several other formats besides HTML. To create the HTML documentation, use the following command:

make doc

Note that the xsltproc commands to produce the document seem as of the time of this writing to be very processor-intensive, and take several minutes to execute on the development system.

2.1.2. Building the Code

To produce the Alba executable program, and as a consequence, the Ada source code files, the makefile uses the tangle stylesheet to produce the Ada source, then invokes gnatmake to build the executable. Since its “all” target is set up to do this, you can invoke this sequence with this simple command:

make

That should produce an executable, usually called alba (but which may have other names on other platforms), which is the Alba program itself. See the reference-manual section for details on how to run it.

In contrast to making the documentation, tangling the source is a pretty speedy operation with xsltproc: on the 700mHz Athlon system where Alba was initially developed, the entire tangling and compilation process took around 12 seconds.

You can also make any individual Ada source file, or several of them, by naming them on the make command line:

make input.ads input.adb alba.adb

Note that if you name sources like that, the makefile does not automatically compile them.

2.2. The Easy Way

In addition to the official XML source files, the distribution includes a package containing the pre-woven documentation and pre-tangled source code. The documentation is standard HTML and should be viewable by any number of tools, such as Lynx or Mozilla. The source code is standard Ada, and should build correctly using whatever technique you use to build Ada programs on your system. On the author's Debian GNU/Linux system, GNAT is the favored compiler, and Alba can be built with this command:

gnatmake alba

That should produce an executable, usually called alba (but which may have other names on other platforms), which is the Alba program itself. See the reference-manual section for details on how to run it.

3. Running Alba

3.1. Executing Alba

Running Alba is very simple--it has no command-line arguments, requires no configuration files, and has a very simple command set. Simply type its name, though in many systems, you must specify that you want to run a program from the current directory by adding “./” to the beginning, like this:

./alba

Note that Alba issues no prompt to tell you it is expecting input. There is a discussion of this design choice elsewhere in this document.

3.2. Alba Command Set

Once Alba is running, you may issue a few simple commands to exercise its functions. Here is a summary of its command set:

c data - Simulate an internal command
d data - Simulate a database access request
f data - Simulate a file access request
q - Quit the simulator

Leading spaces before and after the command letter are ignored. The maximum length of a command line is determined by the length of the input line declared by the input task.

For those commands which accept data (c, d, and f), any string may be given. It is sent to the appropriate subordinate task for processing:

  • The c command simulates an “internal” command, one that Allegra (remember that Alba is a simulation of Allegra) knows how to process without reference to any external data. The command task marks it as such and sends it directly to the output task.
  • The d command simulates a database access request, one that requires a read or write of Allegra's external database. The command task sends the command's data to the database access task; in this simulator, the database task simply marks it as a database request and sends it on to the output task.
  • The f command simulates a file access request, one that requires a read or write of an external file other than Allegra's database. The command task sends the command's data to the file access task; in this simulator, the file task simply marks it as a file access request and sends it on to the output task.

The q command simulates a shutdown command as issued by Allegra's operator, either from the IRC channel or the operator's console. This command ignores any data string given to it, and terminates the program.

4. The Alba Program Code

4.1. Design Rationale

In his book Ada as a Second Language, Norman Cohen says, “Concurrent programming is more difficult than sequential programming. There are many pitfalls for the unwary programmer.” These statements are quite true, but in the case of an application like Allegra, dividing the program into several tasks serves to simplify the design, as well as make the application more flexible. Some advantages are:

  • The overall program structure is simplified by having multiple threads of execution. Accepting input from the IRC server and from the operator console, sending pings, sending responses back to the server, all need to be done, and deciding which thing gets done at which time could get quite hairy. Plus, they each need to happen when they need to happen, not when a single-threaded program manages to get around to them.
  • Responsiveness is enhanced. If a sudden burst of traffic comes from the server, it will just pile up in the command queue, while the rest of the program's operations continue to happen in a timely manner.
  • Each task's code is simplified, because it concentrates on its specific job, without worrying about the other things the whole program has to do. The input task, for example, need concern itself only with reading data from the socket that is connected to the IRC server, deciding whether it is addressed to the bot, and if so, stuffing it into the command queue. Period. It has no other concerns than this, and so can be quite simple in and of itself.
  • Because the tasks are each focused on a single, simple subset of the processing, their external dependencies are correspondingly fewer. The input task does not interact with the database, so it does not require any visibility of the database interface libraries. The ping task does not need standard I/O, because its only job is to insert periodic requests into the output queue. And so on.
  • Because the tasks are so strongly partitioned, the interfaces between them are well defined. This makes it easier to add to or modify the program, by replacing modules (tasks) or altering their behavior. If the operator console needs to run remotely, the existing console task can be replaced with one that makes a CORBA connection, for example. Or if Allegra were to be enhanced to handle multiple IRC servers (don't count on it!), multiple input and output tasks could be created, one set to handle each server, with minimal impact on the rest of the program code.

Yes, some of these benefits are not specifically related to the concurrent nature of the task structure, but rather can be attributed to the modular nature of the Ada language itself. Reducing external visibility to make the program more manageable is done via package structure, not tasks. But thinking of the tasks as little separate programs unto themselves is a mental adjustment that makes it easier (at least for me) to see the separations between the various different portions of the code.

4.2. Alba's Task Structure

Here is an illustration showing the structure of Alba's (and Allegra's) tasks, and the communications among them.

Figure 1. Task Structure

Task Structure

Some clarification of the diagram is in order:

  • Each task “sits on” something. That is, it is usually blocked waiting for some event to occur or some input to arrive. In the case of the four tasks with associated queues, they wait until an item is added to their queue. In the case of the input task and the console task, they block on an I/O stream, denoted by the small circle on their inputs. And the ping task blocks on a delay statement, denoted by the small clock icon within the task's box.
  • The zig-zag “communication link” lines connecting to the IRC server denote a single socket. One line is used only for input, the other only for output. In Alba, the role of the IRC server is played by the standard input and output streams.
  • The dotted line connecting the input task to the ping task is the task-entry call that the input task makes to let the ping task know that input has been received.
  • Since Alba does not actually connect to a remote server, it is locally controlled directly by its input task. Thus the console task is omitted in Alba.

In Allegra, the input task waits for text from the IRC server. It decides whether that input is directed at Allegra specifically, and if so, passes it along to the command task. The command task parses these commands and decides what to do with them: handle them itself, or pass them along to its helpers, the database and file tasks. If it is an “internal” command, the command task places any output it generates directly onto the output queue itself. If it's handled by a helper task, those tasks in turn place their output onto the output queue. Periodically, the ping task puts a ping message onto the output queue. If the operator wishes to control the bot locally, s/he gives a command to the console task, who places it onto the command queue. And finally, the output task takes requests from its queue, formats them, and writes them to the IRC server.

The broad summary of Allegra's execution is modeled by Alba, with a few changes made for simplicity's sake. In Alba, input is read from the standard input stream instead of an IRC server, output is written to the standard output stream instead of an IRC server, and there is no console task. All input is assumed to be directed at the program, so no input is ignored as it is in Allegra. No actual inspection of the data is performed beyond command recognition. No database or files are read or written. Alba's design and structure are described in greater detail in the following sections.

4.3. The Input Task

The input task is the starting point for all on-line activity. In Alba, the socket connection to the IRC server is represented by the standard input stream, allowing the user to play the role of the IRC server, using a set of simple commands to simulate actions and exercise the various parts of the program. In Allegra, this task has the additional responsibility of deciding whether a given message from the server is actually intended for the bot, or is regular channel traffic. This decision is required because the bot appears to the server as just another IRC user, and all users on a channel get all messages sent to the channel by every user. For Alba, every input from the user is intended for the program, so no such decision need be made. Once input is recognized as being directed at the bot, it is formatted (very simply) and placed on the command task's request queue.

4.3.1. Input Loop

The heart of the input task is an endless loop, which reads input, processes it (in this case, simply by converting it to the command-request format), and places the request on the command queue.

§4.3.1.1: §4.3.3.1

      loop
         Get_Line (Input_Line, Input_Length);
         Ping.Ping_Task.Input_Received;
         Command_Request.Data := To_Unbounded_String (Input_Line (1 .. Input_Length));
         Command.Requests.Enqueue (Command_Request);
      end loop;

The task spends most of its time sitting on the Get_Line statement, waiting for the user to enter a command. This simulates the real program's behavior of sitting on a socket read. The loop is terminated when the task is aborted by the command task, when it recognizes a “quit” command.

Note that the input task does not issue any prompt to let the user know that it is ready to accept input. If we simply added a Put or Put_Line call before the Get_Line, the results would be unsatisfactory, if perhaps instructive. (Yes, we tried it during development. Don't laugh.) The problem is, because of the concurrent nature of task execution, the order in which the prompt, and the output from the previous command, are written to the standard output stream is indeterminate. Just as often, the prompt is written before the pending output line, producing a very unappealing and confusing transcript.

Even if we used the output queue instead of direct I/O calls, the order could not be predicted without additional synchronization among the tasks. Such a mechanism would be simple to implement, but since there is no analogue of it in Allegra, it was not deemed sufficiently important to be included. So you just have to trust that Alba is ready to accept input after execution begins. If you absolutely must have a prompt, well, you have the source, so have fun!

4.3.2. Input Task Declarations

The input loop uses these three variables:

  • Input_Line - The string which contains the input from the user. This simulates a message from the IRC server, though the commands have a much simpler format than IRC messages. Its size is set by the Input_Line_Max constant. Its value is set by the standard Get_Line library function.
  • Input_Length - The length in characters of the line the user has typed. Set by the standard Get_Line library function.
  • Command_Request - The request variable to be placed on the command task's request queue. Its value is simply copied from the input line via the To_Unbounded_String standard library procedure.

§4.3.2.1: §4.3.3.1

      Input_Line_Max:  constant := 256;

      Input_Line:       string (1 .. Input_Line_Max);
      Input_Length:     natural;
      Command_Request:  Command.Request_Rec;

The value for Input_Line_Max was chosen arbitrarily, as being “big enough” to accomodate any line the user is likely to type during testing. If the user enters more characters than this, it will be returned as multiple lines. So, don't do that!

4.3.3. Input Task Body

The task's body contains only the processing loop, and its required declarations.

§4.3.3.1: §4.3.4.1

   task body Input_Task_Type is
§4.3.2.1. Input Task Declarations
   begin  -- Input_Task_Type
§4.3.1.1. Input Loop
   end Input_Task_Type;

4.3.4. Input Task Package

For compilation purposes, the task is contained in a package. The task is its only content, since no other packages depend on it. The external dependencies are:

  • Ada.Text_IO - Provides the procedure Get_Line, which is used to read commands from the user.
  • Ada.Strings.Unbounded - Provides the function To_Unbounded_String, used to convert the fixed-length input command string to the unbounded-string format required by the command task.
  • Command - The command task's package, which provides the data type for the command-task queue requests, and the Enqueue procedure, which adds the request to the command request queue.
  • Ping - The ping task's package, which provides the Input_Received task entry, which the input task uses to tell the ping task that some input was seen coming from “the link” (in Alba's case, the user).

Visibility is provided to the standard library packages via a use clause; references to items declared in the Command package must be fully qualified.

§4.3.4.1

with
   Ada.Text_IO,
   Ada.Strings.Unbounded,
   Command,
   Ping;

use
   Ada.Text_IO,
   Ada.Strings.Unbounded;

package body Input is
§4.3.3.1. Input Task Body
end Input;

The input task's package spec is similar to that of the ping task; they are both a little different from the specs of the other task packages. Since the command task needs to be able to abort the input task when a “quit” command is processed, it needs a way to identify the input task to the abort statement. Thus the input and ping task package's specs declare a task type, and a variable of that type, instead of the anonymous task types of most of the other tasks.

§4.3.4.1

package Input is
   task type Input_Task_Type;
   Input_Task:  Input_Task_Type;
end Input;

4.4. The Output Task

The output task is the portal through which all output intended to be sent to the IRC server is channeled. (I hate to use the word “portal” due to the over-use it has undergone in the context of the World-Wide Web, but it's the right word to use here.) In Alba, the socket connection to the IRC server is represented by the standard output stream, allowing the user to play the role of the IRC server, visually verifying that the various parts of the program are functioning correctly.

4.4.1. Output Loop

The heart of the output task is an endless loop. It removes a request from the output queue and checks the Operation field to determine whether the task is being asked to shut down. If so, the loop, and thus the task, terminates. If not, it converts the Data field to a format that can be written to its output stream, and writes it.

§4.4.1.1: §4.4.3.1

      loop
         Requests.Dequeue (Request);
         exit when Request.Operation = Shutdown_Operation;
         Put_Line (To_String (Request.Data));
      end loop;

The task spends most of its time sitting on the Dequeue statement, waiting for an output request to be placed in its queue by one of the other tasks.

4.4.2. Output Task Declarations

The output loop uses only the Request variable, which is a local item of the type of output-queue entries, used to receive the next item taken from the request queue.

§4.4.2.1: §4.4.3.1

      Request:  Request_Rec;

Its Operation field has one of these values:

  • Shutdown_Operation when the command task has processed a “quit” command, in which case the Data field is undefined.
  • Output_Operation when the Data field contains a string to be written to the output stream.

4.4.3. Output Task Body

The task's body contains only the processing loop, and its required declarations:

§4.4.3.1: §4.4.4.1.1

   task body Output_Task is
§4.4.2.1. Output Task Declarations
   begin  -- Output_Task
§4.4.1.1. Output Loop
   end Output_Task;

4.4.4. Output Task Package

4.4.4.1. Output Package Body

For compilation purposes, the task is contained in a package. The package body contains only the (anonymous) task object. The external dependencies are:

  • Ada.Text_IO - Provides the procedure Put_Line, which is used to write strings to the standard output stream for the user to inspect.
  • Ada.Strings.Unbounded - Provides the function To_String, used to convert the unbounded output string to the fixed-length string format required by the standard output library.

Visibility is provided to the standard library packages via a use clause; references to items declared in the PQueue package must be fully qualified.

§4.4.4.1.1

with
   Ada.Text_IO,
   Ada.Strings.Unbounded;

use
   Ada.Text_IO,
   Ada.Strings.Unbounded;

package body Output is
§4.4.3.1. Output Task Body
end Output;

4.4.4.2. Output Package Spec

The output request queue and its supporting types are declared in the package spec so they can be used by other packages.

4.4.4.2.1. Output Package Spec Basic Type Declarations

The spec declares the output-queue request record type and its supporting operation type. It contains an operation (the Operation field) and its data (the Data field), which is not used by all operations. The data is the string to be written to the output stream, if the operation is Output_Operation. The Unbounded_String type of the Data field is defined via the context clause. The spec also declares the task which comprises the body of the package.

§4.4.4.2.1.1: §4.4.4.2.4.1

   type Operation_Type is ( Shutdown_Operation, Output_Operation );

   type Request_Rec is record
      Operation:  Operation_Type;
      Data:       Unbounded_String;
   end record;

   task Output_Task;

4.4.4.2.2. Output Queue Declaration

The spec instatiates the generic library package PQueue using Request_Rec as its data-item type, and then declares the actual output queue itself, as an item of that new queue type.

§4.4.4.2.2.1: §4.4.4.2.4.1

   package Output_Queue_Pkg is new PQueue (Request_Rec);

   Requests:  Output_Queue_Pkg.Protected_Queue_Type;
4.4.4.2.3. Output Package Spec Context Clause

The output task's package spec depends on these two packages:

  • Ada.Strings.Unbounded, which provides the Unbounded_String type used for the request record's data.
  • PQueue, a local library package, which is not defined in this document. It is a generic package which implements a simple protected queue type. The code was adapted from Cohen's book Ada as a Second Language.

Visibility is provided to the standard library package via a use clause; references to items declared in the PQueue package (whose instantiation is called Output_Queue_Pkg) must be fully qualified.

§4.4.4.2.3.1: §4.4.4.2.4.1

with
   Ada.Strings.Unbounded,
   PQueue;

use
   Ada.Strings.Unbounded;

4.4.4.2.4. Package Spec Proper

The package spec puts together the context clause and the declarations to make a compilation unit.

4.5. The Command Task

The command task is the brains of the program. It interprets input commands and dispatches requests for information to the database and file access tasks, as well as handling some commands itself. In Alba, the input comes from the user on the standard input stream; in Allegra, commands come from users on the IRC server.

4.5.1. Command Loop

The heart of the command task is an endless loop. It removes a request from the command queue, parses its content as a command, then executes the command. The command set for Alba is very simple, consisting of an initial letter that determines where the remaining data in the string is to be dispatched. In addition, it recognizes a “quit” command, causing it to send shutdown operations to its subordinate tasks, manually abort the ping and input tasks, and then terminate its loop, and thus its task, causing the termination of the application process.

§4.5.1.1: §4.5.3.1

      loop
         Requests.Dequeue (Request);
§4.5.1.1.1. Command Parsing
§4.5.1.2.1. Command Execution
      end loop;

The task spends most of its time sitting on the Dequeue statement, waiting for a command request to be placed in its queue by one of the other tasks. (In Alba, only the input task puts requests on the command queue.)

4.5.1.1. Command Parsing

Since the commands have a very simple format, parsing them is straightforward. The parser scans for the first non-blank character, then interprets that as a command. (If no non-blank character is found, the request is discarded.) The parser then scans for the next non-blank after the command letter, and uses the rest of the string starting with that character as the data, if the command requires data. If there isn't a non-blank character after the command character, then the data is set to the null string.

§4.5.1.1.1: §4.5.1.1

         Cmd_At := Index_Non_Blank (Request.Data);
         if Cmd_At > 0 then
            Command := Element (Request.Data, Cmd_At);
            if Cmd_At < Length (Request.Data) then
               Data := To_Unbounded_String (Slice (Request.Data, Cmd_At + 1, Length (Request.Data)));
               Data_At := Index_Non_Blank (Data);
               if Data_At > 0 then
                  Data := To_Unbounded_String (Slice (Data, Data_At, Length (Data)));
               else
                  Data := Null_Unbounded_String;
               end if;
            else
               Data := Null_Unbounded_String;
            end if;
         end if;

The Index_Non_Blank function returns the character position of the first character within its unbounded-string argument that is not a space character; its operation, and those of the other unbounded-string functions used in this code, are described in the Ada Reference Manual, section A.4.5. If no non-blank character is present, the function returns zero.

The Element function is used to fetch the command character from the unbounded Data string, once its position has been determined.

The test Cmd_At < Length (Request.Data) determines whether any data string follows the command character. If it is false, no data was given, and the data string is set to the constant Null_Unbounded_String, causing the eventual user, the output task, to print nothing.

Once the command character has been found, the Slice function is used to return the remainder of the string, so it can be scanned for the next non-blank character, which is taken as the first character of actual data. The To_Unbounded_String function must be used, since Slice returns a plain fixed-length string value. This “tail” of the input line is stored temporarily back in the variable Data.

Slice is used once again when (if) a non-blank character is found following the command character, to extract the actual data string starting with its first non-blank character. It is again converted to an unbounded string and stored back in Data.

4.5.1.2. Command Execution

The command having been parsed into the Command and Data variables, the command task now attempts to recognize and execute the command. The case statement that recognizes the command has four types of alternative.

  • The first alternative is the “internal command” handler. In this simulator, all that does is prepend the string “Internal command: ” to the input string and place it on the output queue.
  • The next two alternatives are “dispatch” commands. Their commands send a data string to the database access task or the file access task.
  • The fourth alternative processes the “quit” command. It dispatches shutdown messages to those subordinate tasks which have request queues, aborts the input and ping tasks (which do not have request queues), and then terminates itself. This causes the application to exit.
  • The final alternative handles all unrecognized commands, by adding an error message to the output queue.

§4.5.1.2.1: §4.5.1.1

         case Command is

            when 'c' =>
               Output_Request.Operation := Output.Output_Operation;
               Output_Request.Data := "Internal command: " & Data;
               Output.Requests.Enqueue (Output_Request);

            when 'd' =>
               Database_Request.Operation := Database.Database_Operation;
               Database_Request.Data := Data;
               Database.Requests.Enqueue (Database_Request);

            when 'f' =>
               File_Request.Operation := File.File_Operation;
               File_Request.Data := Data;
               File.Requests.Enqueue (File_Request);

            when 'q' =>
               Database_Request.Operation := Database.Shutdown_Operation;
               File_Request.Operation     := File.Shutdown_Operation;
               Output_Request.Operation   := Output.Shutdown_Operation;
               Database.Requests.Enqueue (Database_Request);
               File.Requests.Enqueue     (File_Request);
               Output.Requests.Enqueue   (Output_Request);
               abort Input.Input_Task;
               abort Ping.Ping_Task;
               exit;

            when others =>
               Output_Request.Operation := Output.Output_Operation;
               Output_Request.Data := To_Unbounded_String ("Unrecognized command '" & Command & "' ignored");
               Output.Requests.Enqueue (Output_Request);               

         end case;

It may be argued that it would be “cleaner” in some ways for the input and ping tasks to have their own “back-channel” queues, to accept the shutdown command at the very least, if not other as yet unforeseen command operations, instead of having the command task forcibly “shoot them in the head” as it does now. However, monitoring both that queue and their primary input source (the IRC socket, in the case of the input task, and the clock in the ping task) would complicate the logic, and seems a high price to pay for something that can be solved in this fairly straightforward manner using standard Ada facilities. Suggestions are welcome, however.

4.5.2. Command Task Declarations

The command loop uses these variables:

  • Request - A local item of the type of the command-queue entries, used to receive the next item taken from the request queue. It has only one field, Data, which contains the command string to be parsed.
  • Database_Request, File_Request, and Output_Request - Request-queue variables for each of the three subordinate tasks that the command task talks to. Used by the command execution code to build requests to send to other tasks.
  • Cmd_At and Data_At - Indices within the command string of the location of the command character and the start of the data, respectively. Used by the parser to locate the separate pieces of the command.
  • Command - The command letter, set by the parser and used by the execution code to determing which action to take.
  • Data - The data portion of the command string, set by the parser and used by the execution code when building an outgoing request.

§4.5.2.1: §4.5.3.1

      Request:           Request_Rec;
      Database_Request:  Database.Request_Rec;
      File_Request:      File.Request_Rec;
      Output_Request:    Output.Request_Rec;
      Cmd_At:            natural;
      Data_At:           natural;
      Command:           character;
      Data:              Unbounded_String;

4.5.3. Command Task Body

The task's body contains only the processing loop, and its required declarations:

§4.5.3.1: §4.5.4.1.1

   task body Command_Task is
§4.5.2.1. Command Task Declarations

   begin  -- Command_Task
§4.5.1.1. Command Loop
   end Command_Task;

4.5.4. Command Task Package

4.5.4.1. Command Package Body

For compilation purposes, the task is contained in a package. The package body contains only the (anonymous) task object. The external dependencies are:

  • Ada.Strings.Unbounded - Provides several functions, and one constant, used by the command processor during recognition and execution of the commands.
  • Database, File, and Output - Lets the command task see the request queues of these other tasks, and place requests on them.
  • Input and Ping - Lets the command task see these task objects so they can be aborted when the program terminates.

Visibility is provided to the standard library package via a use clause; references to items declared in the local packages must be fully qualified.

§4.5.4.1.1

with
   Ada.Strings.Unbounded,
   Database,
   File,
   Input,
   Output,
   Ping;

use
   Ada.Strings.Unbounded;

package body Command is
§4.5.3.1. Command Task Body
end Command;

4.5.4.2. Command Package Spec

The command request queue and its supporting types are declared in the package spec so they can be used by other packages.

4.5.4.2.1. Command Package Spec Basic Type Declarations

The spec declares the command-queue request record type and its supporting operation type. The command request record contains only a data string (the Data field). The data is the command as issued by the user. The Unbounded_Stringtype of the Data field is defined via the context clause. The spec also declares the task which comprises the body of the package.

§4.5.4.2.1.1: §4.5.4.2.4.1

   type Request_Rec is record
      Data:       Unbounded_String;
   end record;

   task Command_Task;

4.5.4.2.2. Command Queue Declaration

The spec instatiates the generic library package PQueue using Request_Rec as its data-item type, and then declares the actual command queue itself, as an item of that new queue type.

§4.5.4.2.2.1: §4.5.4.2.4.1

   package Command_Queue_Pkg is new PQueue (Request_Rec);

   Requests:  Command_Queue_Pkg.Protected_Queue_Type;
4.5.4.2.3. Command Package Spec Context Clause

The command task's package spec depends on these two packages:

  • Ada.Strings.Unbounded, which provides the Unbounded_String type used for the request record's data.
  • PQueue, a local library package, which is not defined in this document. It is a generic package which implements a simple protected queue type. The code was adapted from Cohen's book Ada as a Second Language.

Visibility is provided to the standard library package via a use clause; references to items declared in the PQueue package (whose instantiation is called Command_Queue_Pkg) must be fully qualified.

§4.5.4.2.3.1: §4.5.4.2.4.1

with
   Ada.Strings.Unbounded,
   PQueue;

use
   Ada.Strings.Unbounded;

4.5.4.2.4. Package Spec Proper

The package spec puts together the context clause and the declarations to make a compilation unit.

4.6. The Ping Task

The ping task is a simple process that sends periodic ping messages to the IRC server, causing the server to respond and verifying that the link is still functional. In Allegra, the absence of these ping responses will be used to restart the server link. In Alba, it simply submits its messages to the output queue.

4.6.1. Ping Loop

The heart of the ping task is an endless loop, which waits a certain amount of time for the command task to signal that some input has been received. If such a signal is received before the end of the waiting interval, the task notes that fact by resetting its “missed-input” counter to zero, and then begins waiting again. If no such signal is received before the interval has passed, it checks to see if too many such timeouts have occurred. If so, it sends one sort of message, which represents the action in Allegra of resetting the link to the server. If not, it increments its counter, sends a ping message (in this simulator, simply by placing a different sort of message on the output queue) and waits again.

§4.6.1.1: §4.6.3.1

      loop
         select
            accept Input_Received;
               Missed := 0;
         or
            delay Ping_Delay;
            Missed := Missed + 1;
            if Missed >= Max_Missed_Pings then
               Output_Request.Data := To_Unbounded_String ("Link timed out after " &
                                                           Duration'Image (Ping_Delay * Duration (Missed)) & " seconds");
               Missed := 0;
            else
               Output_Request.Data := To_Unbounded_String ("Ping after" & Duration'Image (Ping_Delay) & " seconds");
            end if;
            Output_Request.Operation := Output.Output_Operation;
            Output.Requests.Enqueue (Output_Request);
         end select;
      end loop;

The task spends most of its time sitting on the delay statement, waiting for the interval to expire. The loop is terminated when the task is aborted by the command task, when it recognizes a “quit” command.

The Duration'Image attribute produces a human-readable form of the ping duration value, which is rather ugly, having a huge number of decimal places by default. But the alternative (using the standard generic package Ada.Text_IO.Fixed_IO to format the value into a string variable) would unnecessarily complicate the code, so this method was deemed acceptable.

4.6.2. Ping Task Declarations

The ping loop uses these constants and variables:

  • Max_Missed_Pings - A constant giving the maximum number of timeouts that are allowed before the link is considered “dead”.
  • Ping_Delay - A statically initialized variable giving the length of the delay interval in seconds.
  • Missed - A statically initialized variable which counts the number of delay intervals which have passed without receiving any input.
  • Output_Request - The request variable to be placed on the output task's request queue. Its value is set to a predefined string. In Allegra, it would be a valid IRC “ping” message.

§4.6.2.1: §4.6.3.1

      Max_Missed_Pings: constant := 2;
      Ping_Delay:  Duration := 60.0;

      Missed: natural := 0;
      Output_Request:  Output.Request_Rec;

In Alba, Ping_Delay is set to one minute, so as not to clutter the output with too many ping messages. Presumably it will be shorter in Allegra.

4.6.3. Ping Task Body

The task's body contains only the processing loop, and its required declarations.

§4.6.3.1: §4.6.4.1

   task body Ping_Task_Type is
§4.6.2.1. Ping Task Declarations
   begin  -- Ping_Task_Type
§4.6.1.1. Ping Loop
   end Ping_Task_Type;

4.6.4. Ping Task Package

For compilation purposes, the task is contained in a package. The task is its only content, since no other packages depend on it. The external dependencies are:

  • Ada.Strings.Unbounded - Provides the function To_Unbounded_String, used to convert the fixed-length ping message to the unbounded-string format required by the command task.
  • Output - The output task's package, which provides the data type for the output-task queue requests, and the Enqueue procedure, which adds the request to the output request queue.

Visibility is provided to the standard library package via a use clause; references to items declared in the Output package must be fully qualified.

§4.6.4.1

with
   Ada.Strings.Unbounded,
   Output;

use
   Ada.Strings.Unbounded;

package body Ping is
§4.6.3.1. Ping Task Body
end Ping;

The ping task's package spec is similar to that of the input task; they are both a little different from the specs of the other task packages. Since the command task needs to be able to abort the ping task when a “quit” command is processed, it needs a way to identify the ping task to the abort statement. Thus the ping and input task package's specs declare a task type, and a variable of that type, instead of the anonymous task types of most of the other tasks. In addition, the ping task has an entry, used by the input task to signal the arrival of input.

§4.6.4.1

package Ping is
   task type Ping_Task_Type is
      entry Input_Received;
   end Ping_Task_Type;
   Ping_Task:  Ping_Task_Type;
end Ping;

4.7. The File Task

The file task accepts requests for access to files on the host filesystem where the bot is executing. In Alba, it simply reports that such requests have been received, by placing a message on the output queue; in Allegra, once the task is actually implemented, it will read and write files as directed by the requests it receives.

4.7.1. File Request Processing Loop

The heart of the file task is an endless loop. It removes a request from the file request queue and examines its Operation field. If it is a file access request operation, it adds some identifying text to the data string and places the resulting string on the output queue. If it is a shutdown request, the loop terminates, which terminates the task.

§4.7.1.1: §4.7.3.1

      loop
         Requests.Dequeue (Request);
         exit when Request.Operation = Shutdown_Operation;
         Output_Request.Operation := Output.Output_Operation;
         Output_Request.Data := "File request: " & Request.Data;
         Output.Requests.Enqueue (Output_Request);
      end loop;

The task spends most of its time sitting on the Dequeue statement, waiting for a file access request to be placed in its queue by the command task.

4.7.2. File Task Declarations

The file loop uses these variables:

  • Request - Local variable of the type of the file-queue entries, used to receive the next item taken from the request queue.
  • Output_Request - Request-queue variable for the output task. Used to report requests received by the file task.

§4.7.2.1: §4.7.3.1

      Request:           Request_Rec;
      Output_Request:    Output.Request_Rec;

4.7.3. File Task Body

The task's body contains only the processing loop, and its required declarations:

§4.7.3.1: §4.7.4.1.1

   task body File_Task is
§4.7.2.1. File Task Declarations

   begin  -- File_Task
§4.7.1.1. File Request Processing Loop
   end File_Task;

4.7.4. File Task Package

4.7.4.1. File Package Body

For compilation purposes, the task is contained in a package. The package body contains only the (anonymous) task object. The external dependencies are:

  • Ada.Strings.Unbounded - Provides the “&” operator, used to concatenate the informational message to the request data..
  • Output - Lets the file task see the request queue of the output task, so it can make output requests.

Visibility is provided to the standard library package via a use clause; references to items declared in the Output package must be fully qualified.

§4.7.4.1.1

with
   Ada.Strings.Unbounded,
   Output;

use
   Ada.Strings.Unbounded;

package body File is
§4.7.3.1. File Task Body
end File;

4.7.4.2. File Package Spec

The file request queue and its supporting types are declared in the package spec so they can be used by other packages.

4.7.4.2.1. File Package Spec Basic Type Declarations

The spec declares the file-queue request record type and its supporting operation type. In Allegra, if the operation field is File_Operation, the data field is a file access request; in Alba, it's any arbitrary text. The Unbounded_String type of the Data field is defined via the context clause.

§4.7.4.2.1.1: §4.7.4.2.4.1

   type Operation_Type is ( Shutdown_Operation, File_Operation );

   type Request_Rec is record
      Operation:  Operation_Type;
      Data:       Unbounded_String;
   end record;

   task File_Task;

4.7.4.2.2. File Queue Declaration

The spec instatiates the generic library package PQueue using Request_Rec as its data-item type, and then declares the actual file queue itself, as an item of that new queue type.

§4.7.4.2.2.1: §4.7.4.2.4.1

   package File_Queue_Pkg is new PQueue (Request_Rec);

   Requests:  File_Queue_Pkg.Protected_Queue_Type;
4.7.4.2.3. File Package Spec Context Clause

The file task's package spec depends on these two packages:

  • Ada.Strings.Unbounded, which provides the Unbounded_String type used for the request record's data.
  • PQueue, a local library package, which is not defined in this document. It is a generic package which implements a simple protected queue type. The code was adapted from Cohen's book Ada as a Second Language.

Visibility is provided to the standard library package via a use clause; references to items declared in the PQueue package (whose instantiation is called File_Queue_Pkg) must be fully qualified.

§4.7.4.2.3.1: §4.7.4.2.4.1

with
   Ada.Strings.Unbounded,
   PQueue;

use
   Ada.Strings.Unbounded;

4.7.4.2.4. Package Spec Proper

The package spec puts together the context clause and the declarations to make a compilation unit.

4.8. The Database Task

In Allegra, the database task accepts requests for access to the bot's database. In Alba, it simply reports that such requests have been received, by placing a message on the output queue.

4.8.1. Database Request Processing Loop

The heart of the database task is an endless loop. It removes a request from the database request queue and examines its Operation field. If it is a database access request operation, it adds some identifying text to the data string and places the resulting string on the output queue. If it is a shutdown request, the loop terminates, which terminates the task.

§4.8.1.1: §4.8.3.1

      loop
         Requests.Dequeue (Request);
         exit when Request.Operation = Shutdown_Operation;
         Output_Request.Operation := Output.Output_Operation;
         Output_Request.Data := "Database request: " & Request.Data;
         Output.Requests.Enqueue (Output_Request);
      end loop;

The task spends most of its time sitting on the Dequeue statement, waiting for a database access request to be placed in its queue by the command task.

4.8.2. Database Task Declarations

The database loop uses these variables:

  • Request - Local variable of the type of the database-queue entries, used to receive the next item taken from the request queue.
  • Output_Request - Request-queue variable for the output task. Used to report requests received by the database task.

§4.8.2.1: §4.8.3.1

      Request:           Request_Rec;
      Output_Request:    Output.Request_Rec;

4.8.3. Database Task Body

The task's body contains only the processing loop, and its required declarations:

§4.8.3.1: §4.8.4.1.1

   task body Database_Task is
§4.8.2.1. Database Task Declarations

   begin  -- Database_Task
§4.8.1.1. Database Request Processing Loop
   end Database_Task;

4.8.4. Database Task Package

4.8.4.1. Database Package Body

For compilation purposes, the task is contained in a package. The package body contains only the (anonymous) task object. The external dependencies are:

  • Ada.Strings.Unbounded - Provides the “&” operator, used to concatenate the informational message to the request data..
  • Output - Lets the database task see the request queue of the output task, so it can make output requests.

Visibility is provided to the standard library package via a use clause; references to items declared in the Output package must be fully qualified.

§4.8.4.1.1

with
   Ada.Strings.Unbounded,
   Output;

use
   Ada.Strings.Unbounded;

package body Database is
§4.8.3.1. Database Task Body
end Database;

4.8.4.2. Database Package Spec

The database request queue and its supporting types are declared in the package spec so they can be used by other packages.

4.8.4.2.1. Database Package Spec Basic Type Declarations

The spec declares the database-queue request record type and its supporting operation type. In Allegra, if the operation field is Database_Operation, the data field is a database access request; in Alba, it's any arbitrary text. The Unbounded_String type of the Data field is defined via the context clause.

§4.8.4.2.1.1: §4.8.4.2.4.1

   type Operation_Type is ( Shutdown_Operation, Database_Operation );

   type Request_Rec is record
      Operation:  Operation_Type;
      Data:       Unbounded_String;
   end record;

   task Database_Task;

4.8.4.2.2. Database Queue Declaration

The spec instatiates the generic library package PQueue using Request_Rec as its data-item type, and then declares the actual database queue itself, as an item of that new queue type.

§4.8.4.2.2.1: §4.8.4.2.4.1

   package Database_Queue_Pkg is new PQueue (Request_Rec);

   Requests:  Database_Queue_Pkg.Protected_Queue_Type;
4.8.4.2.3. Database Package Spec Context Clause

The database task's package spec depends on these two packages:

  • Ada.Strings.Unbounded, which provides the Unbounded_String type used for the request record's data.
  • PQueue, a local library package, which is not defined in this document. It is a generic package which implements a simple protected queue type. The code was adapted from Cohen's book Ada as a Second Language.

Visibility is provided to the standard library package via a use clause; references to items declared in the PQueue package (whose instantiation is called Database_Queue_Pkg) must be fully qualified.

§4.8.4.2.3.1: §4.8.4.2.4.1

with
   Ada.Strings.Unbounded,
   PQueue;

use
   Ada.Strings.Unbounded;

4.8.4.2.4. Package Spec Proper

The package spec puts together the context clause and the declarations to make a compilation unit.

4.9. The Main Procedure

Since all of the real processing of Alba is done in the various tasks, the main procedure has a null body, and terminates immediately. The application continues to execute until all its tasks have terminated, which occurs when the user enters a “quit” command. Its main reason for being is to cause the elaboration of the task packages.

§4.9.1

with
   Command,
   Database,
   File,
   Input,
   Output,
   Ping;

procedure Alba is
begin  -- Alba
   null;
end Alba;

Note that Alba does very little error-checking. Specifically, no exception handling is defined, so if a task's code raises an exception, that task will simply terminate and its queue will fill up with unprocessed requests if the user continues to issue requests for that task. If the input task terminates while the rest of the tasks continue to run, Alba essentially becomes useless and must be killed by an OS operation, such as the kill command. This lack of robustness was deemed acceptable in a technology demonstrator.