CooPLa Coordination Patterns Language

The CooPLa language was thought for software architects. It endows them with a tool for, in the abstract, design the coordination between components or services in the context of service-oriented architectures, or more generically, component-based software.

Coordination is the fundamental activity in software architectures design. It defines how each component of the system will interact with the others. In this sense, a coordination structure encapsulates the policies and protocols that restrict and guide the communication between the components.

While there are several approaches to abstract how coordination structures are defined (e.g., tuple-space, message-driven, event-driven, etc.) one of the most acclaimed is the channel-based. Traditionally, a channel-based approach assumes the coordination to be made from the exterior of the components (exogenous coordination), i.e., the components are agnostic of their surroundings, including with which other components they will interact. Moreover, it is also assumed that channels are building-blocks that can be composed to form complex coordination structures and that each channel presents exactly two ends where data is written and read by the attached components or other channels.

Some coordination approaches also take into account the performance aspects of the systems. This is to face the needs of constructing dependable systems, where performance is usually a challenging characteristic. In the channel-based approaches of coordination, channels are overloaded with notions of quality of service, notably, their processing delays (i.e., the time needed process data).


The model underlying CooPLa is set on a channel-based approach for coordination. It takes a loose definition of channel, though: channels have more than two ends; and it does not assume either any specific coordination formalism like Reo or BIP; the behaviour of a channel is abstractly defined via automata. The model underlying CooPLa, is essentially a graph where edges are channels with an identifier and a behaviour, and nodes are either points for composition of other channels or points for attaching components (the so called interface of the coordination structure).

CooPLa enables the definition of channels, patterns and (stochastic) instances as the grammar below shows:

coopla              : import* element* 
element             : channel_def | pattern_def | stochastic_def
import              : 'import' FILE_PATH ';'

channel_def         :'channel' channel_sig extension? '{' channel_body '}'
channel_sig         : ID dimensions? '(' ports? ':' ports? (':' ID '=' condition)? ')'
dimensions          : '@' ( ID | INT )
                    | '~' ( ID | INT ) ( ',' ( ID | INT ) )*
ports               : ID ( ',' ID )*
condition           : '<' ID ( ',' ID )* '>'
extension           : 'extends' ID
channel_body        : state_def? ( flow_def ';' )+ 
state_def           : 'state' ':' ID ';' 'observers' ':' ID ( ',' ID )* ';'
flow_def            : requests '->' flow_type
requests            : request ( ',' request )* 
request             : '!'? ID
flow_type           : normal_flow ( '|' normal_flow)? flow_label  
                    | ID '?' flow_type : flow_type 
normal_flow         : 'flow' ( ID | 'NULL' ) 'to' '@'? ( ID | 'NULL' )
flow_label          : '#' ID

pattern_def         : 'pattern' pattern_signature '{' pattern_body '}' 
pattern_signature   : ID '(' ports? ':' ports? ')'
pattern_body        : 'use' ':' pattern_decls 'in' ':' pattern_comps
pattern_decls       : ( reference_sig 'as' ID (',' ID)* ';' )+
reference_sig       : ('(' ID ')')? channel_sig
pattern_comps       : ( port_definition ';' )+ ( join_operation ';' )+ 
port_definition     : ID '=' (p_acc | join_operation)
p_acc               : ID '.' ID
join_operation      : 'join' port_access_list 'as' ID
                    |  'xor' port_access_list2 'as' ID
port_access_list    : '[' p_acc ( ',' p_acc )* ']'
port_access_list2   : '[' p_acc+ ':' p_acc ',' p_acc (',' p_acc)* ']'

stochastic_def      : 'stochastic' ID  '@' stochastic_list ID
stochastic_list     : '{' stoch_elem+ '}'  
stoch_elem          : ID ('#' ID)? '=' stoch_val ';'   
stoch_val           : FLOAT
                    |  '(' FLOAT ',' FLOAT ')'  

Channels

A channel is defined by the reserved word channel followed by a signature and a body. A signature mainly defines the ID of the channel (its name), and the input and output ports (separated by :).

In the signature, one may further specify whether the channel is

  • structured*: a dimension ~N of the internal structure of the channel has to be specified after the channel name. This is used for channels that present internal state;
  • timed: a time @T has to be specified after the channel name. This is used for imposing delays in data transmission;
  • conditioned: a pattern condition cond=<...> has to be specified after the channel ports (separated from the output ports with a :. This is used to condition the behaviour of a channel to certain data structures.
  • extension: a base channel has to be specified after the pattern ports (and conditions). The construction extends < channel_name > is used to define it. In this case, the channel inherits the behaviour of the base channel.

* Whenever a channel is structured, a definition of that structures has to be specified in the body of the channel. This definition assumes

  • the structure name: state: < sate_name > and
  • the set of state observers (predicates over the structure): observers: < set_of_names >.

These names are assumed to be references to data structures and methods defined in an external programming language.


Besides the structure definitions, the body of a channel is essentially a list of flows. Each flow define how the channel behaves to stimuli (intentions to read from and write to the channel ends) on its interface (and internal structure, if present). A flow is specified as P->F, where P is a set of channel ends (representing where stimuli exist) and/or state observers evaluating to true (possibly negated with !).

A flow may be

  • normal: follows the form flow p1 to p2, where p1,p2 are ports of the channel, the state name or the special point of the channel where data is lost or automatically produced NULL. This specifies that data flows (atomically) from point p1 to point p2;
  • atomically-synchronised: follows the form f1 | f2, where f1,f2 are normal flows;
  • conditioned: follows the form cond ? f1 : f2, where f1,f2 are normal flows and cond is the data structure pattern condition defined in the channel signature.

Each flow may have associated a flow label (#< label >), which is used for further reference to that flow. For instance, it is used to assign a stochastic value describing the processing delay performance characteristic of that flow.

A timed channel will directly specify a clock in its flows. Example, flow p1 to@T p2 specifies that data flow from point p1 to point p2, after T units of time have passed.



Examples

channel sync(a:b) {
  a,b -> flow a to b #ab;
}         

The sync channel is read as a structure with input port a and output port b. Whenever there are IO requests pending at both ports simultaneously, then data flows from a to b.

channel lossy(a:b) extends sync {
  a,!b -> flow a to NULL  #aL;
}             

The lossy channel extends sync with an extra flow: whenever there is a write (input) request at port a and no read (output) request at port b (notice the use of ! to convey negation), then data is lost (\ie it flows from a to NULL).

channel fifo~N(a:b){
  state: buffer; observers: E, F;     
  a,!F -> flow a to buffer # aB;
  !E,b -> flow buffer to b #Bb;
}            

The fifo channel is a structure with input port a, output port b, a state named buffer with dimension N and observers E and F that check whether buffer is (E)empty and (F)ull, respectively. The behaviour of this channel is defined taking into account the pending requests at the ports as well as the configuration of its internal structure.

channel drain(a,b:)  {
  a,b -> flow a to NULL  
                | 
        flow b to NULL  #ab;
}            

The drain channel expects simultaneous stimuli at its two input ports; whenever this clause is fulfilled, data flows atomically synchronised (notice the use of construct |) from each of these ports to NULL, being lost.

channel shiftFifo~N(a:b) extends fifo {
  a, F -> flow buffer to NULL 
                   |  
          flow a to buffer #aBF;
}           

The shiftFifo channel inherits from the fifo channel. Notice that interface is equal and it also defines an internal structure, but it does not redefine the reference name and the observers for such structure. Behaviourally, it defines a new flow rule expressing that whenever there is an input stimulus at port a and the buffer is full, a datum in buffer is lost and the datum in a flows to buffer.

channel filter(a:b:c=<_,_>) {
  a,b -> c ? flow a to b #ab
                  : 
            flow a to NULL #aL ; 
}           

The filter channel presents a signature added of a datatype pattern c that matches data of sort pair (with components of any sort). This pattern is used in the definition of its flow rule. The latter expresses that whenever there are pending requests at both ports of the channel, if the sort of the data to be written matches pattern c, then data flows from a to b; otherwise it is lost.

channel timed@T (a:b) {
  a,b -> flow a to@T b #ab;
}          

The timed channel is defined with a clock. Such clock imposes a delay T on the flow of data from ports a to b, whenever there are IO stimuli on the channel interface.

channel router (a: b,c) {
  a,b,!c -> flow a to b #ab;
  a,c,!b -> flow a to c #ac;
  a,b,c  -> flow a to b #allab;
  a,b,c  -> flow a to c #allac;
}          

The router is presented as a structure with one input port and two output ports. Notice also the existence of two flow rules that are triggered whenever there are pending requests at all the channel ports. This explicitly expresses non-deterministic behaviour of the channel. Consequently, data will flow non-deterministically from port a to either port b or port c.

Patterns

A pattern (i.e., a coordination pattern) is defined by the reserved word pattern followed by a signature and a body. A signature defines the ID of the pattern (its name), and the input and output ports (separated by :).

The body of a pattern defines the interconnections between channels and previously defined patterns. This is where the coordination graph is obtained. Two phases are considered: the instantiation and the configuration.


Instantiation phase

The instantiation phase is initiated by the reserved word use:. In this phase channels and patterns are instantiated as variables are in traditional programming languages.

The instantiation takes the full signature of the channel (or pattern) to define the type of the instances. The full signature is used to provide logic names to refer to the ports of the channels (or patterns), as well as to define concrete values for structure dimensions, clock times and data structure condition patterns. For channels that define a structure it is mandatory to specify which state observer holds at its initial configuration.


Configuration phase

The configuration phase is initiated by the reserved word in:. In this phase the graph of elements is assembled after the concrete definition of the pattern interface.

The definition of the pattern interface is the simple assignment of the logical instance ports to the names of the pattern ports specified in the signature. The logical ports instances are accessed via el.p, where el is the name of an instance declared in the instantiation phase and p is a logical name given to one of the instance ports.

The assembling of the coordination structure is essentially made by joining ports of the instances: join [p1,p2...] as j, where p1,p2,... are accesses to the ports of the instances, and j is a new graph node. CooPLa also provides xor [p1,p2,... : p3,p4,...] as x to perform the assembly of instances. Both constructs define new graph nodes. The latter adds a semantic overload of an exclusive or data router. This is, in fact, a shorthand to defining a channel or pattern with such a behaviour (e.g., router channel) and connect its ports to the ports of other channels.



Example

pattern Sequencer (a : b, c) {
  use :
    sync(i:o) as s1, s2, s3;
    (E)fifo~1(i:o)  as f1;
  in:
    a  = s1.i
    b  = s2.o; 
    c  = s3.o;
    join [s1.o, s2.i, f1.i] as hij;
    join [f1.o, s3.i] as kl; 
}         

The Sequencer pattern is defined as a structure with input port a and output ports b and c.

In the instantiation phase, there are defined three sync channels with logical reference names i and o, to the channel ends a and b, as defined in the Channels tab sync example. Similarly, one instance of the fifo channel is declared. Notice the concrete definition of a structure dimension and the initial state of the internal structure.

In the configuration phase, the first three lines define how the interface of the Sequencer pattern is attached to the composing instances. The remaining lines define two nodes of the graph that result by the junction of the ends of the channel instances.

This simple example shows everything about coordination patterns definition.

Stochastic Instances

A stochastic instance (of a coordination pattern) is defiend by the reserved word stochastic followed by the name of the pattern of which the instance belongs, list of its stochastic values, and the ID of the instance (its name).

A stochastic instance is a coordination pattern assigned with concrete stochastic values that define processing delays of channels, read and writing delays of nodes and environment information (in particular stimuli arrivals) that is associated to the interface of that pattern. In CooPLa, it is assumed that delays and arrivals are modelled by exponential distributions. Since these distributions only require a parameter -- the rate, a floating point value expressing the number of events occur per unit of time -- then this is the value expected by CooPLa in the list of stochastic values of the created instance.

The stochastic values are assigned to ports, nodes and channels following notation name @ value, name is one of

  • port -- a reference to a port in the interface of the pattern being instantiated;
  • node -- a reference to a node as created by the join or xor operations and
  • channel -- a reference to a flow label defined in the body of each channel that is part of the pattern being instantiated. In this case, the name is n#l, where n is the name of an instance of a channel declared in the pattern instantiation phase and l is a label defined in the body of that channel.
and value is either a floating point number for ports and channels, and a pair (r,w) for nodes, where r and w are also floating point numbers modelling the delay of reading and writing data from and to a channel, respectively.



Example

stochastic Sequencer {
  a       @ 100.0;
  b       @ 10.0;
  c       @ 90.0;
  s1#ab   @ 1000.0;
  s2#ab   @ 500.50;
  s3#ab   @ 1000.0;
  f1#aB   @ 980.45;
  f1#Bb   @ 1500.0;
  hij     @ (100000.0, 150.50);
  kl      @ (100000.0, 99.0);
} sseq  

In this example, a stochastic instance of a Sequencer coordination pattern with name sseq is specified.

The first three lines define the rate at which stimuli from the environment arrive to each of the ports of that instance. For example, stimuli arrive to each port at a rate of 100 per unit of time.

The next five lines define the channel delays. Notice how the labels defined for the examples in the Channels tab are accessed here. These values express, in fact, how many items the channel is able to process per unit of time. Using the inverse of each number it is obtained the time that the channel needs to process each data time.

The final two lines define the stochastic values for the internal nodes. These are not required to be expressed, because it is normal to assume that internal nodes read and write data instantaneously. However, CooPLa allows to define them in order to more faithfully represent real world systems.