Fractal | Fractal ADL | Cecilia Examples | Minus
 

Introduction

Purpose

The task framework provides a tool for organizing and executing the ultimate operations (e.g. code generation, deployment, etc.) that are supported by the Fractal ADL toolset. It is designed as a facility to implement the Processing module described in the Cecilia ADL toolchain . Given a correct AST as output of the Loader module, the purpose of the Task framework is first to build a task graph out of the AST visiting phase. At the end of this AST visit, the task graph is expressing all dependencies between processing tasks. This task graph can then be executed safely in a second phase, allowing for a clean separation of concerns. The task framework implements a hierarchical model that allows designers to compose complex operations from more simple ones. For instance, the whole glue code generation operation that is required for composing Fractal architectures using the Cecilia framework may be modeled as a composite task, so that it can be composed from fine-grain tasks, each implementing a specific code generation feature (e.g. server, client interfaces, component type, component instance, etc.).

Overview

The processing operations that are supported by an ADL compilation toolchain may be arbitrarily complex. When it is required to support complex operations, designers would compose them from simple operations. The purpose of the task framework presented in this section is, precisely, to fulfill this requirement. Indeed, this framework provides means for encapsulating the processing operations and to model the data they exchange with their environment, whatever their complexity is.

In addition, a composition mechanism is supported to allow designers to compose their complex operations. Using this composition mechanism, designers may enjoy some design-time features such as modeling and verification (e.g. compatibility of dependent tasks, presence of execution cycles, etc.) and execute the overall set of operations in a correct order.

This framework is based on a component-based approach, where tasks are modeled and implemented as components. The Fractal Component Model is chosen in order to use a simple and hierarchical model that allows representing arbitrarily complex processing operations. According to this model, tasks may be either primitive or composite (i.e. composed from other tasks). They interact with their environment through client and server interfaces. Using client interfaces, tasks may access to data that is provided by other tasks. Symmetrically, tasks may give access to the result of their execution through their server interfaces. Data dependencies between tasks may be modeled using bindings. Data exchange between tasks can only happen if their client and server interfaces are bound together. This way, the capture of bindings between tasks makes the data dependencies explicit, and the resolution of the binding graph leads to the determination of a correct scheduling order at each composition level.

Some linguistic tools are provided for helping the implementation of the tasks. First of all, a flexible type system is used for specifying the characteristics of the input/output data of each task. That is, each task interface can be annotated with a record , i.e. a collection of field/value pairs, in order to specify ( with as much details as needed) the data exchanged through. Second, an annotation system is provided for describing the architecture of primitive components (i.e. client/server interfaces with their records) within their implementation source files. Finally, a composition language is provided for describing the composition rules of composite tasks.

Primitive Tasks

Overview

The tasks which are implemented in Java programming language are called primitive tasks. Primitive tasks are indeed annotated Java classes that are structured as components, i.e. they implement and invoke well defined interfaces for interacting with their environment.

Annotation System

The annotation system that is used for implementing primitive tasks is based on Java Annotations . They allow to capture additional information related to task interfaces and to their parameters. This information is required for introducing the primitive components into the task composition language presented in Section Link .

TaskParameters

@TaskParameters is an annotation that needs to be attached to each task implementation class. It allows the definition of a list of parameters that may be used for characterizing a task. These parameters are typically used for specifying record values that are not fixed at design time, but at the tasks instantiation time.

The below listing presents the signature of the @TaskParameters annotation. According to this signature, an array of String values may be defined as parameters of a task implementation class. The default value of this annotation is an empty list. Henceforth, tasks without parameters may be annotated with @TaskParameters shorthand.

					
public @interface TaskParameters {
  String[] value() default {};
}
				

ServerInterface

@ServerInterface is an annotation that allows the definition of a server interface implemented by a task implementation class. The below listing presents the signature of the @ServerInterface annotation.

					
public @interface ServerInterface {
  String name();
  Class<?> signature();
  String record();
  String[] parameters() default {};
}
				

The members of this annotation are:

  • name : The name of the server interface. This information may be useful for tracing and debugging operations.
  • signature : The signature of the server interface. The value of this member must correspond to the class of the annotated server interface (e.g. SourCodeProvider.class ).
  • record : The record specifying the data that flows through the server interface. Its value must be a String containing field/value pairs separated by a comma (' , '). Fields and values are separated by a block (' : '). The special ' % ' keyword may be used as value for referencing a parameter.
  • parameters : The array of parameters that are used in the above record specification. The declaration order of the parameters must correspond to the order in which the parameters are referenced in the record definition. Each parameter declared in this member must be already declared as a task parameter presented in Link .

ServerInterfaces

@ServerInterfaces is an annotation that allows the definition of the list of server interfaces implemented by a task implementation class. Each task implementation class needs a @ServerInterfaces annotation to be attached. The signature of this annotation is presented in the below listing. According to this signature, an array of @ServerInterface annotations may be defined.

					
public @interface ServerInterfaces {
  ServerInterface[] value();
}
				

ClientInterface

@ClientInterface is an annotation that allows the definition of a client interface of a task implementation class. A client interface may be either singleton or composite. The latter property is detected using the type of the interface field to which the @ClientInterface annotation is attached. These field types must respect the following conventions:

  • singleton : the interface field must be declared as a public and non-final field of the implementation class. Its type should correspond to the type of the interface. In the case of a singleton client interface, the @ClientInterface annotation may not contain the signature attribute, since this can be discovered from the type of the interface field.
  • collection: the interface must be a public and final Map . The type of the key must be String for containing the name of each member of the client interface. The type of the value must correspond to the type of the client interface. The field must be initialized in the implementation class with an empty object implementing the Map interface.

The signature of the @ClientInterface annotation is presented in the below listing.

					
public @interface ClientInterface {
  String name();
  Class<?> signature() default DefaultSignature.class;
  boolean contingency() default TypeFactory.MANDATORY;
  String record();
  String[] parameters();
}
				

The members of this annotation are:

  • name : The name of the client interface. This information may be useful for tracing and debugging operations.
  • signature : The signature of the client interface. The value of this member must correspond to the class of the client interface (e.g. SourCodeProvider.class ).
  • contingency : The contingency specification of the client interface. It may be either mandatory ( TypeFactory.MANDATORY ) or optional( TypeFactory.OPTIONAL ). If no contingency member is specified, the interface is considered as mandatory by default. The contingency information may be used for verifying the correctness of task graph compositions.
  • record : The record specifying the data that flows through the server interface. Its value must be a String containing field/value pairs separated by a comma (' , '). Fields and values are separated by a block (' : '). The special ' % ' keyword may be used as value for referencing a parameter.
  • parameters : The array of parameters that are used in the above record specification. The declaration order of the parameters must correspond to the order in which the parameters are referenced in the record definition. Each parameter declared in this member must be already declared as a task parameter presented in Link .

ClientInterfaceForEach

@ClientInterfaceForEach is an annotation that allows the definition of a collection of singleton client interfaces. This way, one can (1) iterate over the set of client interfaces, as in the case of collection client interfaces, and (2) distinguish each interface making part of the collection using a key (i.e. an element of Iterable ) object. The interface field must be a public and final Map . The type of the key must correspond to the Iterable element that is used for distinguishing the client interfaces making part of the collection. The type of the value must correspond to the type of the client interface. The field must be initialized in the implementation class with an empty object implementing the Map interface.

The signature of the @ClientInterfaceForEach annotation is presented in the below listing.

					
public @interface ClientInterfaceForEach {
  String iterable();
  String prefix();
  Class<?> signature();
  boolean contingency() default TypeFactory.MANDATORY;
  String record();
  String[] parameters();
}
				

The members of this annotation are:

  • iterable : The name of a task parameter whose value is an iterable object containing the list of key values that can be used for accessing the Map of client interfaces (e.g. List implementedInterfaces ).
  • prefix : The name prefix that is common to each client interface belonging to the collection. This information may be useful for tracing and debugging operations.
  • signature : The signature of the client interfaces belonging to the collection. The value of this member must correspond to the class of the client interfaces (e.g. SourCodeProvider.class ).
  • contingency : The contingency specification of the client interface. It may be either mandatory ( TypeFactory.MANDATORY )or optional( TypeFactory.OPTIONAL ). If the contingency is mandatory, then each client interface belonging to the collection must be bound. If no contingency member is specified, the interface is considered as mandatory by default. The contingency information may be used for verifying the correctness of task graph compositions.
  • record : The record specifying the data that flows through the server interface. Its value must be a String containing field/value pairs separated by a comma (' , '). Fields and values are separated by a block (' : '). The special ' % ' keyword may be used as value for referencing a parameter.
  • parameters : The array of parameters that are used in the above record specification. The declaration order of the parameters must correspond to the order in which the parameters are referenced in the record definition. Each parameter declared in this member must be already declared as a task parameter presented in Link .

Execution of the primitive tasks

The tasks are executed using a classical thread-based model of execution. That is, when a client task invokes its client interface, the receptor component that implements the server end is executed within the thread of the caller task.

However, in a real-world use case, the methods implemented by a task may be invoked multiple times. And, in most cases, the result to be returned for each invocation is the same. For this reason, it is often useful to process the result at once, and return the pre-processed result each time a server interface is invoked.

To deal with this issue, the task framework offers the concept of 'executable tasks'. Executable tasks implement a special interface, called Executable , providing a single method called execute() . If a task implement the Executable interface, then, the first time one of its server interfaces is invoked, its execute() method is executed before executing the receptor interface. Henceforth, the server method implementations may always return values that are processed in the execute() method.

Another benefit of using the Executable interface appears when a task graph is executed by multiple threads. Indeed, the execute method is synchronized , so that it is protected against the re-entrance. However, for any other server interface method, it is up to developers to deal with the re-entrance issues.

Primitive Task Factory

Tasks are instantiated using a factory component. The interface for instantiating primitive tasks is called PrimitiveTaskFactory . Its signature is depicted in the below listing. This interface provides a single method, called newPrimitiveTask() that accepts as input parameter (1) the implementation of the primitive task and (2) the set of values to be assigned as its parameters, and that returns the Component interface of the instantiated task component. TaskException by this method is raised if an error occurs during the instantiation.

				
public interface PrimitiveTaskFactory {

  Component newPrimitiveTask(final Object implementation,
      final Object... parameters) throws TaskException;
}
			

Composite Tasks

Overview

The tasks that are implemented by composing other tasks are called composite tasks. In other words, composite tasks provide means of implementing complex tasks from more simple ones. The task framework presented in this document supports a composition language and an interpreter that executes this language in order to provide designers with a mechanism for building composite tasks.

Composition language

The specific composition language described in this section is used for implementing composite tasks from either primitive or composite tasks. It allows describing a set of bindings for connecting the sub-task instances and a set of client and server interfaces to be exported.

A task composition file must contain one and only one composite task description. The name of the composition file must correspond to the simple name of the composite task which is defined in, and must be suffixed by ".task".

A composite task description starts by a list of import_definition statements. These statements are followed by the composite task name, and the list of its parameters. Then, a list of composition instructions are given between braces.

				
<task_definition> ::= <import_definition>*
                      <identifier> <parameter_list> 
                      "{" ( <instruction> )* "}"
			

The import_definition statements allow for importing composition operators to be used in the composition description. These operators are presented in Link . Each import_definition statement defines a simple identifier to be used for referencing a composition operator whose implementation class is indicated by a fully qualified name corresponding to its relative path, considering the class path of the compilation toolchain as basis. Default operators presented in Link doesn't need to be imported; they can be invoked directly.

				
<import_definition> ::= "import" <identifier> "from" <fully_qualified_name> ";"                        

<fully_qualified_name>  ::= <identifier> ( "." <identifier> ) *
			

The parameter_list of a composite task description is a list of identifiers separated by commas. No type information associated to the parameters is specified in the description language. Note that, type compatibilities between the parameters given to the task and the fields to which they are assigned are checked by the factory component during the instantiation of composite tasks.

				
<parameter_list> ::= "(" <identifier>
                         ( "," <identifier> )*
                      ")"
			

There are two kinds of instructions in the composite task description language. These are export and bind instructions.

				
<instruction> ::= <export_instruction>
                  | <bind_instruction>
			

The export instruction allows the description of a matching rule for capturing the interfaces (either client or server ) to be exported outside the composite task. It is composed of two parts separated by the as keyword. The left part of the instruction describes the interfaces to be exported while the right part describes how the latter are to be exposed to the environment. The left part of an export instruction is composed of a record definition that may be optionally followed by a where definition. These two definitions allow the description of the interfaces of sub-tasks to be exported as detailed as needed. The right part is composed only of an external record definition. The external record definition allows defining another record definition to be associated with the exported interfaces. It is optional as well: when the right part of an export instruction is omitted, the internal record definition is exported as it is.

				
<export_instruction> ::= "export" <record_definition>
                         [ <where_definition> ]
                         [ "as" <external_record_definition> ]
                         " ; "
			

A record definition starts optionally with a role definition. A role of an interface may be either client or server . Exporting a client interface allows satisfying the client interfaces of the sub-components, while exporting server interfaces allows exposing the interfaces implemented by sub-components. The specification of a role is only for design and readability purposes. That is, it can also be derived from actual roles of the interfaces that are described using the record definitions.

A record definition is composed of a list of record elements. These record elements describe field/value pairs to be matched for capturing the interfaces to be exported or bound. A specific record element is "...". It matches any field/value pairs that may be specified within a record. When it is specified after a list of record element definitions, it stands for "at least the previous field/value pairs must be matched".

				
<record_definition> ::= [<role_definition>]
                       "{" 
                          ( <record_element_list> [ "," <dot_dot_dot> ] )
                          | <dot_dot_dot> 
                        "}"

<role_definition>   ::= "client"
                         | "server"

<dot_dot_dot>       ::= "..."

<record_element_definition_list> ::= <record_element_definition>
                                     ("," <record_element_definition> )*
			

A record element is a field/value pair. The field name is specified using a simple identifier. The value of a record element may be either a constant, a variable or the result of a function call. Variables are defined using simple identifiers. Such identifiers may possibly match a parameter name. In this case, the value of the variable is associated to the value of the parameter. Otherwise, a new variable is defined implicitly. Such variables may be used in where definition statements as explained below.

				
<record_element_definition>      ::= <identifier> ":" <value>

                         
<value>              ::= <constant>
                         | <variable>
                         | <function_call>
                         
<constant> ::= <string_literal>
               | <integer>
               | <boolean> 
               | <null>

<variable> ::= <identifier>

<function_call> ::= <identifier> "(" <function_parameter_list> ")"

<function_parameter_list> ::= <value> ( "," <value> )*
			

A where definition specifies a boolean value evaluating whether the preceding record expression matches or not. Typically, the where statement may be used for evaluating if two task interfaces making part of a composite component are associated to the same AST node or not. In this case, two variables node1 and node2 may be defined as values of the node field of both interface records and their correspondence may be checked within the where statement, using the same operator (e.g. same(node1,node2) ). If the result of this operator is true , then composition rule matches for those interfaces. Note that, if the boolean value is result of a function call, then the function needs to be already imported at the beginning of the composition file (except the default composition operators presented in Link ) .

				
<where_definition>   ::= "where" <value>
			

An external record definition may only be used as the right part of an export definition. It differs from standard record definitions in the fact that it assigns values to the record fields. These values allow specifying how the record of exported interfaces are exposed to the external environment.

				
<external_record_definition> ::= "{" <record_element_assignment_list> "}"

<record_element_assignment_list> ::= <record_element_assignment>
                                     ("," <record_element_assignment> )*

<record_element_assignment>      ::= <identifier> "=" <value>

			

The second main instruction of the composition language is the bind instruction. It allows the description of a matching rule for capturing the interfaces to be bound together. It is composed of two parts separated by the to keyword. The left part of the instruction describes the client interfaces which will be bound to the server interfaces described in the right part. Each part is formed by a record definition that allows describing the properties of the interfaces to be bound. Finally, a where statement may be defined optionally in order to add a boolean expression for evaluating the relations between the field values of both record definitions.

				
<bind_instruction>   ::= "bind" <record_definition>
                         "to"   <record_definition>
                         [ <where_definition> ]
                         ";"
}
			

Composition operators

As explained in the previous section, some composition operators may be used in composite task descriptions in order to take advantage of evaluation functions executed during the instantiation of the composite tasks. Albeit programmers are free to define their own composition operators, some operators that are frequently used are provided by the task framework. This section first presents these default operators. Then, the programming rules to be considered for developing new operators are explained.

Default composition operators

The default composition operators that are provided by the task framework are enumerated below:

  • And : Accepts a list of boolean values and returns true if all the values are true . Returns false otherwise.
  • Or : Accepts a list of boolean values and returns true if at least one of the values is true . Returns false otherwise.
  • Not : Accepts a boolean value and returns its opposite.
  • Equals : Accepts a list of parameters, and returns true if the values of all input parameters are equal. Returns false otherwise.
  • Same : Accepts a list of parameters, and returns true if all of them are references to the same object. Returns false otherwise.

Definition of new composition operators

New composition operators may be defined by programmers. A specific programming pattern needs to be respected for interfacing these operators with the task composition factory.

According to this programming pattern, each new operator must be implemented within a separate class implementing the Function interface. The Function interface, presented in the listing below, provides a single method, namely invoke() . This method accepts a list of parameters, implements the composition operator, and returns an object corresponding to the result of the operation. It may raise the FunctionExecutionException if an error occurs during the operation.

					
public interface Function {
  Object invoke(Object... params) throws FunctionExecutionException;
}
				

Composite Task Factory

The interface for instantiating composite tasks is called CompositeTaskFactory . Its signature is depicted in the below listing. This interface provides two methods. Both of them are called newCompositeTask() . They accept (1) a collection of subTasks to be composed, (2) the name of the file where the composition scheme is described, (3) a composition context, and (4) the list of values to be assigned to the parameters of the task to be instantiated. They return the Component interface of the instantiated task component. They differ in one parameter that accepts the component name for tracing and debugging purposes. They raise TaskException if an error occurs during the instantiation.

				
public interface CompositeTaskFactory {
  Component newCompositeTask(Collection<Component> subTasks,
      String compositionName, Map<Object, Object> context, Object... parameters)
      throws TaskException;

  Component newCompositeTask(Collection<Component> subTasks,
      String compositionName, String compositeName,
      Map<Object, Object> context, Object... parameters) throws TaskException;
}
			
 
2007-2009 © ObjectWeb Consortium  | Last Published: 2009-04-21 13:39  | Version: 2.1.0