Couriers

Courier processes

Courier processes allow to enrich a service aggregation with context functionalities. They are joint to an aggregation operator and they are executed in between a message reception and its forwarding to the final target service. In a courier it is possible to program any kind of behaviour and they are usually exploited for managing all those functionalities which are not directly pertinent with the target service but they are useful for the network context like authentication and logging.

In the diagram above, we represent a courier process as a black square within an inputPort. A courier process does not alter the connection topology of a circuit but it just enhances the capabilities of an inputPort with a specific set of activities.

The syntax

A courier process is defined in terms of a scope prefixed with the keyword courier followed by the name of the input port where attaching the courier process:

courier <Name of the Input port> {
    ...
}

In the body of the scope, the list of the operations affected by the courier process must be defined. The list of operations follows this syntactical structure:

courier <Name of the Input port> {
    [ <declaration of operation> ] {
        // code
    }

    [ <declaration of operation> ] {
        // code
    }

    [ <declaration of operation> ] {
        // code
    }

    ...
}

where the declaration of the operation can be twofold depending on the operation type: request response or one way:

courier <Name of the Input port> {
    /* request response */
    [ operation_name( request )( response ) ] {
        // code of courier process executed for this operation
    }

    /* one way */
    [ operation_name( request ) ] {
        // code of courier process executed for this operation
    }
}

The statement forward

The statement forward can be used within the courier process code of each operation for delivering the message to the final target, as specified in the input port definition for the given operation. The syntax of the forward is very simple and it just follows the structure of the request response or the one way operation:

courier <Name of the Input port> {
    /* request response */
    [ operation_name( request )( response ) ] {
        // code of courier process executed for this operation
        forward( request )( response )
    }

    /* one way */
    [ operation_name( request ) ] {
        // code of courier process executed for this operation
        forward( request )
    }
}

Example

As an example, try to add the following courier process to the service aggregator of the example described in Aggregation

courier Aggregator {
    [ print( request )( response ) ] {
        println@Console("Hello, I am the courier process")();
        forward( request )( response )
    }
}

Such a courier process is attached to port Aggregator and it is applied only on operation print which comes from the aggregated output port Printer. In the console of the service aggregator, as a result, you will see the string Hello, I am the courier process printed out every time the operation print is called. Concretely, the steps performed by the service aggregator above are the following:

  • receive a message for operation print on input port Printer
  • recognize there is a courier process attached for the input port
  • execute the courier process, passing the request message into the request variable---above, the variablerequest
  • execute the behaviour associated with operation print in the courier process:
    • execute the println operation, printing Hello, I am the courier process
    • execute the forward instruction, forwarding the request to the output port Printer (the correct port is found by matching the interface, i.e., operation and type of the request)
    • wait to receive the response from the forwarded service, in variable response
  • send back the obtained response to the original caller

Courier processes attached to interfaces

Sometimes it could happen that a courier process must be executed for all the operations of the same interface. In these cases could be quite annoying to list all the operations of that interface and write for each of them the same code. In Jolie it is possible to join a courier process to all the operations of a given interface. In this case the syntax is:

courier <Name of the Input port> {
    /* all the request response operations of the interface*/
    [ interface interface_name( request )( response ) ] {
        // code of courier process executed for this operation
        forward( request )( response )
    }

    /* all the one way operations of the interface */
    [ interface interface_name( request ) ] {
        // code of courier process executed for this operation
        forward( request )
    }
}

Instead of specifying the name of the operations it is sufficient to use the keyword interface followed by the name of the interface. All the operations of that interface will be attached of the courier process defined in the body code. It is worth noting that there are two different declarations for request response operations and one way operations just because the formers deal both both request and response messages whereas the latter only with the request one.

Couriers example

Here we extend the example presented in Section Aggregation by adding a logging service which is called into the couriers of the aggregator in order to log all the messages sent to the aggregator port.

The complete code of the example can be checked here. In this case we add courier processes to the interfaces of the aggregated ports where, before forwarding the incoming messages to the target ports we call the logger service by sending the content of the message obtained with the operation valueToPrettyString of service StringUtils.

courier Aggregator {
    [ interface PrinterInterface( request )( response ) ] {
        valueToPrettyString@StringUtils( request )( s );
        log@Logger( { .content = s } );
        forward( request )( response )
    }

    [ interface PrinterInterface( request ) ] {
        valueToPrettyString@StringUtils( request )( s );
        log@Logger( { .content = s }    );
        forward( request )
    }
}

It is worth noting that the output port of service Logger is just one of the output ports available within the service aggregator and it is normally defined like all the others.

outputPort Logger {
    Location: Location_Logger
    Protocol: sodep
    Interfaces: LoggerInterface
}

Thus, a courier process can exploit all the available output ports of the service where it is defined for executing its activities.

Interface extension

Interface extension is a feature of Jolie language which can be used jointly with courier processes in order to extend the message types of the operations of an aggregated port. The interface extension alters the final surface at the aggregating input port without affecting the aggregated one.

Interface extension can be particularly useful when it is necessary to enrich the message types of an aggregated ports due to the presence of a courier process attached to it. It is worth noting that the courier process will manage request and response messages conformant to the extended interfaces, but it will automatically forward messages cleaned from the extended parts.

How to define extension rules

interface extender is the keyword used in Jolie for defining the extending rules to overload the types of a given interface. The syntax follows.

interface extender extender_id {
    OneWay: * | OneWayDefinition
    RequestResponse: * | RequestResponseDefinition
}

The interface extender associates an identifier (extender_id) to a set of extending rules which takes the form of a standard operation declaration with the exception of the usage of the token * for denoting all the operations.

As an example let us consider the following interface extender:

type AuthenticationData: void {
    .key:string
}

interface extender AuthInterfaceExtender {
RequestResponse:
    *( AuthenticationData )( void ) throws KeyNotValid
}

This interface extender must be read in the following way:

  • extends all the request response operations of a given interface with type AuthenticationData in request messages and type void in response messages. The AuthenticationData just adds a node key:string to each request message, whereas the type void does not actually alter the response messages. A new fault called KeyNotValid can be thrown by all the extended request response operations. In case we specify the name of the operation in the interface extender, the rule will be applied only to that operation. In the following example, the rule will be applied only to operations named op1.
type AuthenticationData: void {
    .key:string
}

interface extender AuthInterfaceExtender {
RequestResponse:
    op1( AuthenticationData )( void ) throws KeyNotValid
}

How to apply the extension rules

Interface extenders can only be applied to aggregated output ports. In order to do that the keyword with is used for associating an aggregated output port to an interface extender. The syntax follows:

inputPort AggregatorPort {
    // Location definition
    // Protocol definition
    Aggregates:
        outputPort_1 with extender_id1,
        //    ...
        outputPort_n with extender_idn
}

Such a declaration is sufficient for applying the extension rules. It is worth noting that at the level of courier processes, the statement forward will always erase the extended part of the message before forwarding them to the target port.

Interface extension example

For a better understanding of how aggregation and interface extension work, let us enhance the previous example with an interface extension. The full code can be checked here.

In this example we add a service called authenticator for checking the validity of an access key released to all the client which aims at accessing to the port Aggregator of the service aggregator.

If we look into the service aggregator, we will find the following interface extension:

type AuthenticationData: void {
    .key:string
}

interface extender AuthInterfaceExtender {
    RequestResponse:
        *( AuthenticationData )( void ) throws KeyNotValid
    OneWay:
        *( AuthenticationData )
}

and the following courier processes where a checkKey request is sent to the service Authenticator before forwarding the message to the target output port. Note that in case the key is not valid, a fault KeyNotValid generated by the service Authenticator is automatically sent back to the client:

courier Aggregator {
    [ interface PrinterInterface( request )( response ) ] {
        valueToPrettyString@StringUtils( request )( s );
        log@Logger( { .content = s } );
        checkKey@Authenticator( { .key = request.key } )();
        forward( request )( response )
    }

        [ interface PrinterInterface( request ) ] {
        valueToPrettyString@StringUtils( request )( s );
        log@Logger( { .content = s }    );
            checkKey@Authenticator( { .key = request.key } )();
        forward( request )
    }
}

courier Aggregator {
    [ interface FaxInterface( request )( response ) ] {
        valueToPrettyString@StringUtils( request )( s );
        log@Logger( { .content = s }    );
            checkKey@Authenticator( { .key = request.key } )();
        forward( request )( response )
    }
}

Important Note: as it is possible to see from the code of the aggregator service, the operation faxAndPrint implemented within the aggregator is not managed by the couriers, thus there is not any check of key validation for it.

It is not possible to apply a courier process to the operations actually implemented by the aggregator itself.

This is due to the fact that the aggregation statement, together with the courier definition, is an operator at the level of service network, not at the level of a single service. For this reason, if we need to put under courier processes also the operations which are orchestrating other sub services, it is better to place the final aggregator in a separate component with respect to the operation orchestrator, and then aggregates also it into the final aggregator. Here we report the service circuit as it should be in this case: