First steps

The first version of our application will solve the following problem. We have some customers who want to use the metro for going from a point 1 to a point 2. Some connections exist and the system will find out what the possibilities are in order to satisfy the customers, if possible. This first example is very simple and we will increase the complexity throughout the different steps of this tutorial.

For this given example we will consider 4 bags:

  • Customer that will manage resource tuples formed as (name, location) where name is the name of the customer and location is his/her current location.

  • Destination that will manage resource tuples form as (name, arrival) where the name is still the name of the customer and the arrival is the wished destination.

  • Connection that will manage resource tuples formed as (departure, arrival) where the two fields represent the 2 extremities of the connection.

  • Travel that will manage resource tuples formed as (departure, arrival, name) that will log all the travel that have been done (from departure to arrival) and the related customer.

This is all we need for describing our first toy application.

Involved files

Once you enter in the application's directory (/trom_1) you will find 3 files:

  • quinoa.py
  • init.ls
  • trom.ls

quinoa.py

The first one correspond to the description of all the components (hardware and software) which constitute the distributed application. You can give the name you want to this file. We traditionally call it quinoa.py. Indeed, the ancient Incas considered quinoa as the "mother grain" and revered it as sacred. Quinoa.py is the "mother grain" of the your distributed application since it contains everything to create and distribute the various living parts of your application. We recommend you to use this same quinoa.py name as we noticed that the number of bugs is greater for people using a different name. This is due to the malediction of Rascar Capac's 7 Crystal Balls ;-) (sorry just kidding).

In the quinoa.py file, we use the syntax of the python language [http://python.org/] that is quite intuitive. We could have used our own formalism but why reinvent the wheel ?

Basically, the file structure is divided into 3 parts:

  • the physical description of the target distributed system
  • the logical description of the application
  • the mapping of the logical application on the physical system

We introduce hereafter some parts of this file. As you will see, it is not very complex, and even though at first sight some aspects could be considered as strange, you will see that they will become obvious very soon.

All the application's components included in the quinoa.py file are instantiations of specific python object classes.

 # ----- Application
 A1 = data.ApplicationDescription()
 A1.name = "trom"
 A1.path = [TOPDIR]

Here, we describe the application in the A1 object instant, and we define its name :* trom_1. The associated *path property allows to give a list of pathnames in which we may find components from various libraries. Here we only introduce the directory in which the application is stored (TOPDIR is defined upper in the quinoa.py file and initialized with its current storage directory).

Description of the distributed system

 # ----- Physical Distributed System
 # ----- Domain
 domain1 = data.DomainDescription()
 domain1.name = ""
 domain1.bin_directory = BINDIR
 domain1.var_directory = "/tmp"
 domain1.type = "NO_NETWORK"

Here we describe the domains and the machines (hosts) on which the application will be deployed. For this first example, we have decided that we will have a single machine and a very minimal configuration in order to allow you to run this example even if you don't access any network.

We declare a single domain and its description is stored in the domain1 object. The name property allows the name of the administrative domain (e.g. pacull.com) to be captured. Here, to make it simple, and as we will not be using a network just yet, we simply put an empty string for the domain name. We define two directories associated to this domain. The first one, stored in the bin_directory property, is where the middleware is installed on this given domain (BINDIR is defined and initialized upper in the quinoa.py file with a helper global value). The second one, stored in the var_directory property, is a directory where the machines from this domain will be able to write log files and other useful files described later. Then, we set the type of this domain. The type value is a single string defined to differentiate different domains within the same administrative domain (e.g. pacull.com). As an example, in the case of an application distributed over an heterogeneous network, we may define a Unix domain and a Windows domain as they will not use the same convention for the pathnames and could use different locations for the directories we just set. Here, we use "NO_NETWORK" to indicate the fact that we are on a standalone host machine.

We will see later that more detailled information may be added to these descriptions.

Once a domain is defined, the next step is to declare the machines that will be involved.

 # ----- Machines
 H1 = data.HostDescription()
 H1.domain = domain1
 H1.name = "localhost"

Here, we have a single machine localhost attached to our domain domain1. Its description is stored in the H1 object.

Description of the object of the distributed application

 # ----- Logical application
 # ----- Objects
 # NameServer
 NameServer = data.ObjectDescription()
 NameServer.name = "NameServer"
 NameServer.type = "LWNameServer"
 NameServer.port = 9999
 NameServer.host = H1
 NameServer.application = A1
 NameServer.nameserver = NameServer
 LIST_OF_OBJECTS.append(NameServer)

The first object we declare is the NameServer. This is a mandatory object and its role is to store at run time the physical location of the other objects and their included bags. The NameServer is involved each time a logical entity has to be mapped to its running instance. As the NameServer is a predefined component it can be considered as a set of bags containing resources (tuples storing the mapping information). This will be described in more detail later on in the tutorial.

Its type property is set to LWNameServer: a light weight nameserver. This is the simplest form of NameServer and this is enough to meet the requirements of the current application. The port property defines the listening port on which the NameServer will be listening while running. This entry point is required as, for the purpose of this example, we will interact with this nameserver through a web browser.

Then we specify that this NameServer object will run on the host machine H1 and will belong to the A1 application, both described earlier. Finally, with the last append line, we define that this object has to register into itself since it is the application's NameServer.

Once the NameServer is defined, a second object is described hereafter, used as support of our application.

We decided to model this application by using 4 bags, stored in this support object.

 bag1 = data.BagDescription()
 bag1.name = "Customer"
 bag1.type = ("Multiset", "TupleSpace")
 bag1.fieldnames = ['name', 'location']
 bag1.params = {}

The first bag will be known as Customer (as we set its name property to Customer). Its type will be a Multiset implemented into a TupleSpace.

We will see later that it is possible to choose among different kinds of bags that are defined by default:

Bag aspects

or create your own bag:

Create your own bag

with special semantics implemented into different types of storage:

Storage

Here, we have a simple TupleSpace stored into memory. The field names, defined in the list stored in the fieldnames property, describe the form of the tuples managed by this bag.

Following the same rules, 3 other bags are defined, named Connection, Destination and Travel according to the application model described at the beginning of this section.

 bag2 = data.BagDescription()
 bag2.name = "Connection"
 bag2.type = ("Multiset", "TupleSpace")
 bag2.fieldnames = ['depature', 'arrival']
 bag2.params = {}

 bag3 = data.BagDescription()
 bag3.name = "Destination"
 bag3.type = ("Multiset", "TupleSpace")
 bag3.fieldnames = ['name', 'arrival']
 bag3.params = {}

 bag4 = data.BagDescription()
 bag4.name = "Travel"
 bag4.type = ("Multiset", "TupleSpace")
 bag4.fieldnames = ['departure', 'arrival', 'name']
 bag4.params = {}

Then we have to define the object that will host/contain these 4 bags.

 Object1 = data.ObjectDescription()
 Object1.name = "Test"
 Object1.type = "Coordinator"
 Object1.port = None
 Object1.host = H1
 Object1.application = A1
 Object1.nameserver = NameServer
 Object1.params = {param.OBJECT_TRACE_LEVEL:gTraceLevel}
 Object1.list_of_bags = [bag1, bag2, bag3, bag4]
 LIST_OF_OBJECTS.append(Object1)

Here, Object1 is an instance of the object called Test (name property). Its type is set to Coordinator.

A coordinator is a basic object able to enact rules in order to solve a problem. These rules are similar to production rules.

We do not specify any port value for this object to listen to, since we will not have to access it directly.

The host and application properties are set according to the items defined earlier.

Then, the 4 bags described above are "attached" to the object with the help of the list_of_bags list property.

Description of the scripts

The application's logic is defined with scripts containing rules.

In our example, 2 scripts are used.

  • The first one is used to initialize the system. Basically, it sets the initial location of the customers, their destination wishes and the existing connections.
  • The second one is used for describing the problem we want to solve (and the rules that should lead to its resolution !).

The declaration of the scripts in the quinoa.py is given hereafter. The content of the scripts is explained later.

 # ----- Scripts
 script1 = data.ScriptDescription()
 script1.application = A1
 script1.name = "init"
 script1.list_of_objects = [Object1]
 script1.policy = "Automatic"
 LIST_OF_SCRIPTS.append(script1)

 script2 = data.ScriptDescription()
 script2.application = A1
 script2.name = "trom"
 script2.list_of_objects = [Object1]
 script2.policy = "Manual"
 LIST_OF_SCRIPTS.append(script2)

Both script descriptions are instances of ScriptDescription and are attached to application A1. The important information here is the list of objects that will enact the scripts, defined with the list_of_objects property. Here we have only Object1.

The initialization script is named init and the script dealing with our problem and its resolution is named trom. Both scripts are respectively associated with the init.ls and trom.ls files, stored in the current application directory.

Let's examinate the content of these scripts. We will start with the trom.ls script

trom.ls

{*,!}["Test","Customer"].rd(name,departure) &
 {*,!}["Test","Destination"].rd(name,arrival) &
 {*,!}["Test","Connection"].rd(departure,arrival)
 ::
 {
 ["Test","Customer"].get(name,departure) ;
 ["Test","Destination"].get(name,arrival) ;
 ["Test","Connection"].get(departure,arrival) ;
 ["Test","Customer"].put(name,arrival) ;
 ["Test","Travel"].put(departure,arrival,name)
 }.

This script contains a unique rule.

Generally speaking, a rule is composed of 2 parts separated by a ::.

The first part, placed before the ::, is called the precondition part. The part placed after the :: is called the performance part. Basically, the precondition part defines the conditions required for the performance part to be fired.

The requirements expressed in the precondtion are evaluations of availability of specific resources. Thus, the precondition is composed of rd() operations intended to find the resources required to trigger the performance part of the rule.

This performance part is composed of a succession of transactions enclosed in { }. When multiple performance transactions are defined within the same rule (which is not the case here), they are fired (tried) sequentially.

If it is possible to perform all the operations inside the curly brackets, then they are all performed in an atomic manner (all-or-nothing) thanks to a distributed transaction.

Note : at runtime, we may talk about the precondition (or evaluation) phase and the performance phase as each part of a rule is respectively and individually processed.

Let's detail the first line of our trom rule.

  • ["Test","Customer"] defines the bag where we look for resources. Test is the name of the object and Customer the name of the bag. This information will be passed on to the NameServer in order to obtain a stub allowing communication with the expected bag.

  • (name,departure) defines the format of the required resources. The two variables name and departure, once instantiated, will be correctly propagated.

  • {,!}** specifies that we are looking for an infinite number of resources () and that we do not want to set a timeout to get the required resources(!). Alternatively, you may define the number of awaited resources (e.g. 1) or the number of seconds you are ready to wait in case no resource is currently available. **Please note that using numbers both for expecting number of resources and time out can be very misleading and should be avoided unless you really know what you're doing

  • .rd() specifies the read operation that will be performed on the bag.

Let's now unroll the rule with an example.

{*,!}["Test","Customer"].rd(name,departure) will indefinitely look for all the resources corresponding to the customer initial location.

As we want an infinite number of resources, we will obtain one by one all the resources contained in the bag Customer.

When no more resources are available, the stream is blocked until a new resource becomes available. As no timeout is defined, we will wait forever if no new resource appears.

The instantiated value of name, retrieved from the first line of the precondition, will be automatically propagated in line 2 in order to obtain the resource corresponding to the destination of this given customer, destination stored in the ["Test","Destination"] bag.

Information about destination, combined with the initial location of the given customer, will be used to find a connection in the Connection bag (["Test","Connection"]). This search is expressed in the third line of the precondition part.

A fully instantiated rule may look like this :

 ["Test","Customer"].rd("alice", "trocadero") &
 ["Test","Destination"].rd("alice", "raspail") &
 ["Test","Connection"].rd("trocadero", "raspail")
 ::
 {
 ["Test","Customer"].get("alice", "trocadero") ;
 ["Test","Destination"].get("alice", "raspail") ;
 ["Test","Connection"].get("trocadero", "raspail") ;
 ["Test","Customer"].put("alice", "raspail") ;
 ["Test","Travel"].put("trocadero", "raspail", "alice")
 }.

In this case, as all the requirements of the evaluation phase are met (alice is at the trocadero and wishes to go to raspail, which is possible because a connection exists between the two stations), and the distributed transaction of the performance phase will be fired.

In this performance phase, the resources retrieved in the evaluation phase are supposed to be consummed (get operations on the same resources). If some of these resources are no longer available, then the rule is aborted.

In the same way, if it is not possible to set the new location of the customer (insert the new resource : ["Test","Customer"].put("alice", "raspail")) or to record the travel made by alice (insert the new resource : ["Test","Travel"].put("trocadero", "raspail", "alice")), then the transaction is aborted.

If the transaction is aborted, no resource is consumed and no resource is inserted.

Otherwise, if all goes well, all the resources are indeed consumed or inserted according to their place in the rule.

init.ls

This script contains 3 rules which have an empty precondition. This means that their performance part will systematically get triggered.

These rules are triggered once. They are used to initialize the application by inserting initial resources into the required bags.

 ::
 {
 ["Test","Customer"].put("alice","A") ;
 ["Test","Customer"].put("bob","B") ;
 ["Test","Customer"].put("charles","C") ;
 ["Test","Customer"].put("denis","A")
 }.

The previous rule inserts the resources corresponding to the initial location for the various customers.

 ::
 {
 ["Test","Destination"].put("alice","D") ;
 ["Test","Destination"].put("bob","D") ;
 ["Test","Destination"].put("charles","D") ;
 ["Test","Destination"].put("denis","D")
 }.

The second rule inserts the destination of the customers. Here, everybody want to go to destination D

 ::
 {
 ["Test","Connection"].put("A","B") ;
 ["Test","Connection"].put("A","D") ;
 ["Test","Connection"].put("C","D")
 }.

The last rule defines the existing connections.

Running the application

Starting the application

Now let's play a bit with our application.

First we have to start the objects.

For this, open a console window, get to the application's directory (/trom_1) and enter the following command :

 python quinoa.py --start_objects All

The application may display some log lines. When it gives back hand, you can inspect the application's objects. For this, simply open your favorite web browser (Mozilla-Firefox) and navigate to the following url :

http://localhost:9999/NameServer/MONITOR

This allows you to access the monitoring functions embedded by default into any object.

Monitoring the application

The displayed page shows the internal content of the NameServer object. This object is reached through the port on which the object container was listening (9999) and by using the logical name of the name server in the URL path (NameServer). Both these parameters were defined in the quinoa.py file.

It is then possible to see the list of bags of this object. These bags are described later in the tutorial.

By clicking on the logo at the top left-hand corner of the page, you can access a kind of "graphical" representation of all the objects currently running in our application.

Here, we have the NameServer (of type LWNameServer: light-weight nameserver) and the Test object (of type Coordinator, which means that it is able to run scripts, as explained later on). Each object is associated with its host and port information.

By clicking on the Test object representation, you are able to access the internals of this object. You can then see that it contains 8 different bags:

  • 4 bags are specific to the application and have been defined in the quinoa.py file.
  • 3 bags are used for the coordination mechanism and are inherited from the Coordinator type, used as base of the Test object.
  • 1 last bag, present in all objects and used by the system itself.

These 3 types of bags are dedicated to 3 distinct roles the object may be used for : one applicative role, one generic role (for coordination) and one internal role within the system. This exemplifies the fact that the resources-based model is powerful enough to handle these three distinct aspects by just using bags.

The 4 applicative bags are currently empty. As an example, you may click on the Connection one to check this point.

You may now get back to the console window and enter the following command :

 python quinoa.py --list_scripts

The system replies that 2 scripts were declared : one called init.ls and another one called trom.ls (see above).

We have seen that the first one is responsible for the initialization of the application. Let's run it with the following command.

 python quinoa.py --run_scripts init

When the command returns, go back to your web browser and inspect the Customer, Connection and Destination bags: resources have been inserted and are diplayed in the bags' resources monitoring lists:

  • in the Customer bag, the customers of our metro are listed along with their initial location.
  • in the Destination bag, we can monitor their respective destination,
  • in the Connection bag, the list of the connections present in our metro network is available.
  • obviously, no resource is displayed in the Travel bag, as no initialization insertion was included in the init script.

The initial state of the application may be represented in the following table:

| Customer| Destination| Connection| Travel| |---|---|---|---| |('alice','A')| ('alice','D')| ('A','B')|| |('bob','B') | ('bob','D') | ('A','D')|| |('charles','C')| ('charles','D')| ('C','D')|| |('denis','A')| ('denis','D')||

Once the application is initialized, it is time to run the script responsible for the logic of the application.

From the console window, enter the following command:

 python quinoa.py --run_scripts trom

When it's done, by re-inspecting the bags of the application, you may observe that it has moved to the following state.

| Customer| Destination| Connection| Travel| |---|---|---|---| | ('bob','B') | ('bob,'D') | ('A','B')| ('A','D','alice')| | ('denis','A') | ('denis,'D') | |('C','D','charles') | | ('alice','D')||| | ('charles','D')|||

First we see that bob did not move because no connection ('B','D') exists. Second, alice and charles reached their destinations because the ('A','D') and ('C','D') connections existed.

However, you may note something quite strange : denis did not reach his expected destination, despite the fact a resource ('A','D') was initially available...

This situation is a consequence of the fact that only one single resource ('A','D') was initialy inserted in the Connection bag. As it has been consumed by the rule involving alice, it was no longer available for denis.

We should consider this behavior as a bug: our program is not correct for our expressed purpose. Therefore, we have to modify it to make it work as intended.

We have many ways to modify this application. Here are the 2 of them :

  • the first solution is to correct/adapt the rules contained in the trom.ls script. This point is discussed in LINC_Trom_tutorial_-_Coordination_rules_introduction of this tutorial.
  • the second solution is to modify the behavior of the Connection bag. This is the subject of the lesson 04 where we will see how to adapt some aspects of a bag to meet some specific requirements.

Start the application and run the scripts in one command

If you want to start every objects of your application and run every scripts, you may use the following command:

python quinoa.py --go

This will first try to stop the application, in case it's already running. Then it will start all the objects and run all the scripts. For more information on bootstrap commands, refer to LINC boot strap.

Stopping the application

You can now stop the application by executing the following commands.

python quinoa.py --stop_all

This commands asks all the object to stop and wait for them. If this is not enough, you kill your application more brutally like this:

python quinoa.py --kill_all

This command will destroy all the objects of the application. But here, for our stateless toy application, this is not required.

Complementary notes

Actually, the --kill method gets the locally stored PID (Process ID) associated with each object running on the local host (and launched by the current user) and sends a Linux/Unix kill command to each identified process.

To kill a single object, recognized by its name, the following syntax may be used :

python quinoa.py --kill the_object_name
python quinoa.py --kill Test  # kill Test object
python quinoa.py --kill All  # kill all objects of your application

This time, all the processes launched locally by the user are retrieved and killed brutally. This will stop all the local parts of all applications.

Please note that these commands will not kill processes on other hosts in a distributed application.