Aggregation

The Aggregation is an architectural operator between an inputPort and a set of outpurPorts which allows for composing services in a way that the API of the aggregated services are merged with those of the aggregator. It is a generalisation of network proxies that allow a service to expose operations without implementing them in its behaviour, but delegating them to other services. Aggregation can also be used for programming various architectural patterns, such as load balancers, reverse proxies, and adapters.

The syntax for aggregation extends that given for input ports.

inputPort id {
Location: URI
Protocol: p
Interfaces: iface_1, ..., iface_n
[ Aggregates: outputPort_1, outputPort_2, ... ]
}

Where the Aggregates primitive expects a list of output port names.

If we observe the list of the operations available at the inputPort of the aggregator, we will see the list of all the aggregated operations together with those of the aggregator.

How Aggregation works

We can now define how aggregation works. Given IP as an input port, whenever a message for operation OP is received through IP, we have three scenarios:

  • OP is an operation declared in one of the interfaces of IP. In this case, the message is normally received by the program.

  • OP is not declared in one of the interfaces of IP and is declared in the interface of an output port (OP_port) aggregated by IP. In this case the message is forwarded to OP_port port as an output from the aggregator.

  • OP is not declared in any interface of IP or of its aggregated output ports. Then, the message is rejected and an IOException fault is sent to the caller.

We can observe that in the second scenario aggregation merges the interfaces of the aggregated output ports and makes them accessible through a single input port. Thus, an invoker would see all the aggregated services as a single one.

Remarkably, aggregation handles the request-response pattern seamlessly: when forwarding a request-response invocation to an aggregated service, the aggregator will automatically take care of relaying the response to the original invoker.

As an example let us consider the case of two services, the printer and fax, aggregated into one service which also add another operation called faxAndPrint. The code may be consulted here.

The service printer offers two operations called print and del. The former allows for the printing of a document whereas the latter allows for its deletion from the queue. On the other hand the service fax offers just one operation called fax. The aggregator, aggregates on its inputPort called Aggregator both the printer and fax services as it is shown below where we report the ports declaration of the aggregator service:

include "printer.iol"
include "fax.iol"
type FaxAndPrintRequest: void {
.fax: FaxRequest
.print: PrintRequest
}
interface AggregatorInterface {
RequestResponse:
faxAndPrint( FaxAndPrintRequest )( void ) throws Aborted
}
outputPort Printer {
Location: "socket://localhost:9000"
Protocol: sodep
Interfaces: PrinterInterface
}
outputPort Fax {
Location: "socket://localhost:9001"
Protocol: sodep
Interfaces: FaxInterface
}
inputPort Aggregator {
Location: "socket://localhost:9002"
Protocol: sodep
Interfaces: AggregatorInterface
Aggregates: Printer, Fax
}

It is worth noting that the inputPort Aggregator actually offers all the operations available at outputPorts Printer and Fax which are connected with service printer and fax respectively. Moreover, the same inputPort declares also to make available the operations defined into interface AggregatorInterface where one operation is defined: faxAndPrint. As a result, the following operations are available at the inputPort Aggregator:

  • print: which is executed by service printer;

  • del: which is executed by service printer;

  • fax: which is executed by service fax;

  • faxAndPrint: which is executed by the aggregator

In particular, let us notice that the operation faxAndPrint actually orchestrates the operations print and fax in order to provide a unique operation which executes both of them.

The Surface

Here we introduce the concept of surface that is quite similar to that of interface but with some important differences.

A surface is the resulting interface available at a given input port.

interface A { ... }
interface B { ... }
interface C { ... }
interface D { ... }
outputPort Aport {
Interfaces: A
}
outputPort Bport {
Interfaces: B
}
inputPort MyInput {
...
Interfaces: C, D
Aggregates: Aport, Bport
}

In this example there are four interfaces declared: interface A, interface B, interface C and interface D and there are two outputPorts Aport and Bport. The former exploits interface A whereas the latter interface B. There is only one inputPort called MyInput which aggregated both the output ports and also offers interfaces A and B.

In this case the surface at input port MyInput is the resulting interface of the composition of interfaces A, B, C and D.

A surface is always obtained by listing all the available operations and types of all the interfaces available at a given input port. Thus if we calculate the surface of the port Aggregator dicussed in the previous section we will obtain the following one:

type JobID:void{
.jobId:string
}
type FaxAndPrintRequest:void{
.print:PrintRequest
.fax:FaxRequest
}
type PrintRequest:void{
.content:string
}
type PrintResponse:JobID
type FaxRequest:void{
.destination:string
.content:string
}
interface AggregatorSurface {
OneWay:
del( JobID )
RequestResponse:
faxAndPrint( FaxAndPrintRequest )( void ) throws Aborted( undefined ),
print( PrintRequest )( PrintResponse ),
fax( FaxRequest )( void )
}

The surface can be included by an invoker service for getting all the available operations for invoking the port Aggregator.

jolie2surface

One important characteristic of the surface is that it actually does not exist as a software artifact until it is automatically derived and created from an input port declaration. So, how could we create a surface?

The Jolie installation is equipped with a tool called jolie2surface which allows for the creation of a surface starting from a service definition. Its usage is very simple, it is sufficient to run the following command:

jolie2surface <filename.ol> <name of the port>

in order to obtain the surface of port Aggregator discussed in the previous section, the command is:

jolie2surface aggregator.ol Aggregator

if you need to save it into a file, just redirects the standard output:

jolie2surface aggregator.ol Aggregator > surface.iol

Note that the tool jolie2surface also adds the outputPort declaration connected to the input port.

Extracting surface programmatically

The surface can be extracted in a programmatic way too by exploiting the standard library of Jolie. In particular, we can use the services MetaJolie and MetaParser for getting the surface of a an input port of a service. The service MetaJolie provides a set of functionalities for getting important meta information about a service whereas the service MetaParser provides for transforming these information into a syntactically correct Jolie definition. If we want to extract the surface of an input port we can use the operation getInputPortMetaData@MetaJolie which returns a complete description of the input port of a service definition. Then, with the operation getSurface@Parser we can extract the surface by passing the definition of the input port obtained from the previous operation.

In the following you can find the example of the programmatic surface extraction of service aggregator.ol.

include "metajolie.iol"
include "metaparser.iol"
include "console.iol"
main {
getInputPortMetaData@MetaJolie( { .filename = "aggregator.ol" } )( meta_description );
getSurface@Parser( meta_description.input[ 0 ] )( surface );
println@Console( surface )()
}

The executable code can be found at this link

Protocol Transformation

Aggregation can be used for system integration, e.g., bridging services that use different communication technologies or protocols. As an example, let us consider the system discussed in the previous section but considering that the aggregated services offers they operation using different protocols like http/json and http/soap as depicted in the following picture:

In this case the aggregator automatically transforms the messages thus enabling a transparent composition of services which exploit different protocols.

The full executable example can be found here. Here we report the input ports of both the fax and the printer services, and the output ports of the aggregator together with its main input port.

// Fax Service
inputPort FaxInput {
Location: "socket://localhost:9001"
Protocol: soap { .wsdl = "fax.wsdl" }
Interfaces: FaxInterface
}
// Printer Service
inputPort PrinterInput {
Location: "socket://localhost:9000"
Protocol: http { .fomat = "json" }
Interfaces: PrinterInterface
}
// Aggregator
outputPort Printer {
Location: "socket://localhost:9000"
Protocol: http { .fomat = "json" }
Interfaces: PrinterInterface
}
outputPort Fax {
Location: "socket://localhost:9001"
Protocol: soap { .wsdl = "fax.wsdl" }
Interfaces: FaxInterface
}
inputPort Aggregator {
Location: "socket://localhost:9002"
Protocol: sodep
Interfaces: AggregatorInterface
Aggregates: Printer, Fax
}