Table of Contents
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.
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.
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:
The makefile also uses a short perl script, called xweb-include, which is included in the distribution package.
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.
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.
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.
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.
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 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.
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:
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.
Here is an illustration showing the structure of Alba's (and Allegra's) tasks, and the communications among them.
Some clarification of the diagram is in order:
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.
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.
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!
The input loop uses these three variables:
§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!
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; |
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:
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.
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.
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.
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:
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; |
For compilation purposes, the task is contained in a package. The package body contains only the (anonymous) task object. The external dependencies are:
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; |
The output request queue and its supporting types are declared in the package spec so they can be used by other packages.
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; |
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; |
The output task's package spec depends on these two packages:
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; |
The package spec puts together the context clause and the declarations to make a compilation unit.
§4.4.4.2.4.1 |
§4.4.4.2.3.1. Output Package Spec Context Clause package Output is §4.4.4.2.1.1. Output Package Spec Basic Type Declarations §4.4.4.2.2.1. Output Queue Declaration end Output; |
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.
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.)
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.
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.
§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.
The command loop uses these variables:
§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; |
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; |
For compilation purposes, the task is contained in a package. The package body contains only the (anonymous) task object. The external dependencies are:
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; |
The command request queue and its supporting types are declared in the package spec so they can be used by other packages.
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; |
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; |
The command task's package spec depends on these two packages:
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; |
The package spec puts together the context clause and the declarations to make a compilation unit.
§4.5.4.2.4.1 |
§4.5.4.2.3.1. Command Package Spec Context Clause package Command is §4.5.4.2.1.1. Command Package Spec Basic Type Declarations §4.5.4.2.2.1. Command Queue Declaration end Command; |
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.
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.
The ping loop uses these constants and variables:
§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.
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; |
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:
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; |
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.
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.
The file loop uses these variables:
§4.7.2.1: §4.7.3.1 |
Request: Request_Rec;
Output_Request: Output.Request_Rec; |
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; |
For compilation purposes, the task is contained in a package. The package body contains only the (anonymous) task object. The external dependencies are:
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; |
The file request queue and its supporting types are declared in the package spec so they can be used by other packages.
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; |
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; |
The file task's package spec depends on these two packages:
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; |
The package spec puts together the context clause and the declarations to make a compilation unit.
§4.7.4.2.4.1 |
§4.7.4.2.3.1. File Package Spec Context Clause package File is §4.7.4.2.1.1. File Package Spec Basic Type Declarations §4.7.4.2.2.1. File Queue Declaration end File; |
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.
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.
The database loop uses these variables:
§4.8.2.1: §4.8.3.1 |
Request: Request_Rec;
Output_Request: Output.Request_Rec; |
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; |
For compilation purposes, the task is contained in a package. The package body contains only the (anonymous) task object. The external dependencies are:
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; |
The database request queue and its supporting types are declared in the package spec so they can be used by other packages.
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; |
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; |
The database task's package spec depends on these two packages:
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; |
The package spec puts together the context clause and the declarations to make a compilation unit.
§4.8.4.2.4.1 |
§4.8.4.2.3.1. Database Package Spec Context Clause package Database is §4.8.4.2.1.1. Database Package Spec Basic Type Declarations §4.8.4.2.2.1. Database Queue Declaration end Database; |
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.