Sessions

In Jolie a session represents a communication session among the service that holds the session, and other participants. In the majority of cases, a session is enabled when a message is received on a request-response operation, and it is terminated when the response message is sent. In general, more interactions among different participants can be designed establishing a stateful session among them. In Jolie, a stateful session is always represented by a set of data which identifies it within the engine, that is called correlation set. A session is always animated by a specific running process in the engine. More sessions can be carried on by the same process at the same time.

Stateful sessions

Usually a service provides loosely coupled operations, which means that there is no correlation among the different invocations of different operations. Each invocation can be considered as independent from the others. Nevertheless, it could happen that we need to design sessions which can receive messages more than once from external invokers. In these cases, we need to correctly route the incoming messages to the right session.

Let us clarify with an example. Assume a scenario where there is a service which allows two users for playing Tris game (tic tac toe). The Tris game service will keep each game into a specific session. A user can participate to different games, thus it needs to send its move to the right session for correctly playing the game. In this case, we need to route each user message to the right session it is involved in. This is solved by sending for each message an extra information, usually a session identifier. We call these kind of information correlation sets.

If you are curious on seeing how a tris game can be implemented in Jolie, you can find the code at this link

Correlation sets

Jolie supports incoming message routing to behaviour instances by means of correlation sets. Correlation sets are a generalisation of session identifiers: instead of referring to a single variable for identifying behaviour instances, a correlation set allows the programmer to refer to the combination of multiple variables, called correlation variables. In common web application frameworks this issue is covered by sid session identifier, a unique key usually stored as a browser cookie.

Correlation set programming deals both with the deployment and behavioural parts. In particular, the former must declare the correlation sets, instructing the interpreter on how to relate incoming messages to internal behaviour instances. The latter instead has to assign the concrete values to the correlation variables.

In the deployment part the cset is defined as it follows:

cset {
    <variable name>: <List of type paths coupled with the correlation variable>
}

In the behavioural part, the cset is initialized as it follows:

    csets.<variable name> = <variable value>

A precise definition of the syntax can be found in the section below

When a message is received, the interpreter looks into the message for finding the data node which contains the value to be compared with the correlation values. In the diagram above, the variable x is the correlation variable and it can be found in the message of type MyRequest in the subnode x.

In the example of the tris game, the cset is defined in file tris.ol as it follows:

cset {
    token: MoveRequest.game_token
}

where MoveRequest is a message type related to operation move defined in the interface TrisGameInterface.

type MoveRequest: void {
    .game_token: string
    .participant_token: string
    .place: int
}

It is worth noting that the correlation variable is named token but it can be found in node game_token within the type MoveRequest.

Let us now commenting the behavioural part of the example: a game is started by means of operation startGame sent from a user to the tris service. If the startGame message does not contain any token, a new game token is generated by means of the primitive new together a a specific token for each participant, that which plays with circles and that which plays with crosses.

token = new;
...
global.games.( token ).circle_participant = new;
...
global.games.( token ).cross_participant = new;

All these token are stored into an hashmap at the level of a global variable. The circle and the game token are returned to the caller which starts to wait for a contender. When a second user calls the operation startGame by specifying a game token of an existing pending game (retrieved thanks to the operation listOpenGames), the game can be initiated and the second user receives the token for playing with the cross and the game token. At this point, the server calls itself on the operation initiateGame sending all the tokens.

The session started by the invocation of operation initiateGame is actually the game session to which the players must send their moves. Indeed, the fist action performed by such a session is the initialization of the correlation variable token with the actual token of the game:

csets.token = request.game_token;

Finally a loop is started for managing the right moves from the players. Each of them receives the actual status of the game on the operation syncPlaces and they will send their moves using the operation move. As we shown before, the message of operation move contains the node game_token which brings the actual token to be correlated with the variable token.

The primitive new

Jolie provides the primitive new which returns a fresh value to a correlation variable. new guarantees to return a value never returned by one of its previous calls. Its usage is very simple, it is sufficient to assign a variable with the value new:

x = new

Correlation sets syntax

Correlation sets are declared in the deployment part of a program using the following syntax:

cset {
    correlationVariable_1: alias_11 alias_12, ...
    correlationVariable_2: alias_21 alias_22, ...
}

The fact that correlation aliases are defined on message types makes correlation definitions statically strongly typed. A static checker verifies that each alias points to a node that will surely be present in every incoming message of the referenced type; technically, this means that the node itself and all its ancestors nodes are not optional in the type.

For services using sequential or concurrent execution modalities, for each operation used in an input statement in the behaviour there is exactly one correlation set that links all its variables to the type of the operation. Since there is exactly one correlation set referring to an operation, we can unambiguously call it the correlation set for the operation.

Whenever a service receives a message through an input port (and the message is correctly typed with relation to the port's interface) there are three possibilities, defined below.

  • The message correlates with a behaviour instance. In this case the message is received and given to the behaviour instance, which will be able to consume it through an input statement for the related operation of the message.
  • The message does not correlate with any behaviour instance and its operation is a starting operation in the behavioural definition. In this case, a new behaviour instance is created and the message is assigned to it. If the starting operation has an associated correlation set, all the correlation variables in the correlation set are atomically assigned (from the values of the aliases in the message) to the behaviour instance before starting its executing.
  • The message does not correlate with any behaviour instance and its operation is not a starting operation in the behavioural definition. In this case, the message is rejected and a CorrelationError fault is sent back to the invoker.

Another correlation set example

Let us consider an example in which a server prints at console concurrent messages coming from different clients. The complete code can be found here. Each time a client logs in, the server instantiates a unique sid, by means of the new function. To request any other operation (print or logout), each client must send its own sid in order to identify its session with the server.

type LoginRequest {
    name: string
}

type Message {
    sid: string
    message?: string
}

interface PrinterInterface {
    RequestResponse: login( LoginRequest )( Message )
    OneWay: print( Message ), logout( Message )
}

The interface file contains the declaration of operations and data types. Since the sid subtype (OpMessage.sid) will be used as a variable of the correlation set, it is defined as a non-optional subtype (defaulted to [1,1]) and must be present in any message sent and received by all correlated operations.

//printer.ol

from PrinterInterface import PrinterInterface
from PrinterInterface import Message // we need to import also type Message to be used in the cset
from console import Console

service Printer {

    /* here we define the correlation set we will use for correlating messages inside the same session */
    cset {
    /* sid is the variable used inside the session to identify the correlation set.
        The values which come from messages whose type is OpMessage and the node is .sid will be stored
        inside variable sid as identification key */
    sid: Message.sid
    }


    embed Console as Console

    execution: concurrent

    inputPort PrintService {
    location: "socket://localhost:2000"
    protocol: sodep
    interfaces: PrinterInterface
    }

    init {
    keepRunning = true
    }

    main
    {
    /* here the session starts with the login operation    */
    login( request )( response ){
        username = request.name
        /* here we generate a fresh token for correlation set sid */
        response.sid = csets.sid = new
        response.message = "You are logged in."
    }
    while( keepRunning ){
        /* both print and logout operations receives message of type OpMessage,
        thus they can be correlated on sid node */
        [ print( request ) ]{
        println@Console( "User "+username+" writes: "+request.message )()
        }
        [ logout( request ) ] {
        println@Console("User "+username+" logged out.")();
        keepRunning = false
        }
    }
    }
}

cset is the scope containing correlation variable declarations. A correlation variable declaration links a list of aliases. A correlation alias is a path (using the same syntax for variable paths) starting with a message type name, indicating where the value for comparing the correlation variable can be retrieved within the message.

In our example the correlation variable sid is linked to the alias Message.sid.

In scope main, the csets prefix is used to assign a value to the correlation variable sid. The same value is assigned to response.sid (via chained assignment), which is passed as a response to the client.

//client.ol

from PrinterInterface import PrinterInterface
from console import Console
from console import ConsoleInputInterface


service Client {

    embed Console as Console

    inputPort ConsoleInputPort {
    location: "local"
    interfaces: ConsoleInputInterface
    }

    outputPort PrintService {
    location: "socket://localhost:2000"
    protocol: sodep
    interfaces: PrinterInterface
    }

    init {
    registerForInput@Console()()
    }

    main
    {
    print@Console( "Insert your name: " )()
    in( request.name )
    keepRunning = true
    login@PrintService( request )( response )
    opMessage.sid = response.sid
    println@Console( "Server Responded: " + response.message + "\t sid: "+opMessage.sid )()
    while( keepRunning ){
        print@Console( "Insert a message or type \"logout\" for logging out > " )()
        in(    opMessage.message )
        if( opMessage.message != "logout" ){
        print@PrintService( opMessage )
        } else {
        logout@PrintService( opMessage )
        keepRunning = false
        }
    }
    }
}

Finally, in its scope main, the client assigns the sid value to its variable opMessage.sid. It will be used in any other message sent to the server to correlate client's messages to its session on the server.

Correlation variables and aliases

So far we have shown how a correlation variable can be related to a one single subnode of a type, but generally there could be more messages which can contain a value to be correlated. We say there could be more aliases for the same correlation variable. Aliases ensure loose coupling between the names of the correlation variables and the data structures of incoming messages.

Let us consider the following scenario: a chat server allows its users to log in and choose the channel (identified by an integer) they want to join. Once subscribed into a channel a user can send a message, log out from the server or switch channel, by sending another subscription request.

Such a scenario can be modelled by means of four message type definitions (one for each operation), as shown in the snippet below:

// inteface.ol

type LoginType: void {
    .name: string
}

type SubscriptionType: void {
    .channel: int
    .sid: string
}

type MessageType: void {
    .message: string
    .sid: string
}

type LogType: void {
    .sid: string
}

interface ChatInterface {
    RequestResponse:
        login( LoginType )( LogType )
    OneWay:
        subscribe( SubscriptionType ),
        sendMessage( MessageType ),
        logout( LogType )
}

// server.ol

cset {
    sid: SubscriptionType.sid
        MessageType.sid
        LogType.sid
}

It is worth noting that the correlation variable sid is linked to aliases SubscriptionType.sid, MessageType.sid, LogType.sid. Each time the server will receive a correlated-operation request, it will correlate any client to its corresponding session by checking the aliased value of sid.

Multiple correlation sets

Multiple correlation sets can be used in order to manage distributed scenarios. In the authentication example we model the case of an application which delegates the authentication phase to an external identity provider.

The sequence chart https://github.com/jolie/examples/tree/master/v1.10.x/02_basics/5_sessions/authentication of the exchanged messages follows:

First of all the user call the application for requesting a login and it is redirected to the identity provider. Before replying to the user, the application opens an authentication session on the identity provider (calling the operation openAuthentication) which returns a correlation identifier called auth_token. The auth_token is sent also to the user. At this point, the user can sends its credential to the identity_provider together with the auth_token in order to be authenticated. If the authentication has success, the identity_provider sends a success to the application, a failure otherwise. Finally, the user can check if it has access to the application calling the operation getResult, together with the session_id, on the application. The session_id is generated by the application after receiving the reply from the identity_provider.

It is worth noting that the in the application we define two correlation sets:

cset {
    auth_token: OpenAuthenticationResponse.auth_token
                AuthenticationResult.auth_token
}

cset {
    session_id: GetResultRequest.session_id
                PrintMessageRequest.session_id
                ExitApplicationRequest.session_id
}

The former permits to identify the session thanks to auth_token whereas the latter exploits the session_id. Both of them identify the same session, but the token auth_token is used for identifying the messages related to the identity_provider whereas the session_id it is used for identifying the session initiated by the user into the application. Once logged indeed, the auth_token is not used anymore, whereas the session_id can be used by the user for accessing the application. It is worth noting that, after the reception of a success or a failure by the application, the auth_token is still available as a variable inside the session. But, since there are no more operations correlated with it in the behaviour (only the operations getResult, printMessage and exitApplication can be used), it is not possible that the auth_token can be used again for correlating the session.

The provide-until statement

The provide until statement eases defining workflows where a microservice provides access to a set of resources until some event happened. Such a statement is useful in combination with correlation sets, because it allows for accessing a specific subset of operations once a session is established.

The syntax is

provide
    [ IS_1 ] { branch_code_1 }
    [ IS_i ] { branch_code_i }
    [ IS_n ] { branch_code_n }
until
    [ IS_m ] { branch_code_m }
    [ IS_j ] { branch_code_j }
    [ IS_k ] { branch_code_k }

The inputs IS_1, ..., IS_n will be continuously available until one of the operations under the until (IS_m, ..., IS_k) is called.

In the authentication example described in the previous section, the application exploits a provide until for providing the operation printMessage to the final user, until she sends the exiting operation exitApplication:

provide
    [ printMessage( print_request ) ] {
    println@Console("Message to print:" + print_request.message )()
    }
until
    [ exitApplication( request ) ] {
        println@Console("Exiting from session " + request.session_id )()
    }

Sessions and Jolie libraries

Managing session-related calls impacts also on the libraries used by a Jolie program (after all, they are microservices too!). Sometimes it is useful to have a library "call back" its client, e.g., if executing some batch work or waiting for the user's input, for which we do not want to use a request-response pattern, but a one-way: the client enables the reception of some inputs from the library, which then will send a notification to the client each time a new input is ready.

A concrete example of that is operation in of the Console service, which, as seen in the example section on communication ports, receives inputs from the standard input.

While calling that operation on a single-session service does not pose any problem on where to route the incoming request, that is not the case for the concurrent and sequential execution modalities, where many instances can prompt the insertion of some data to the user.

To correctly route input messages to the appropriate session, the Console service puts in place the operation subscribeSessionListener (and its complementary unsubscribeSessionListener). That operation is useful to signal to the Console service that it should "tag" with a token given by the user (more on this in the next paragraph) the input received from the standard input, so that incoming input messages can be correctly correlated with their related session.

Technically, to support that functionality, we need to define a cset targeting the node InRequest.token (visible at the beginning of the code below) and to enable the tagging of input messages by the Console API, calling the operation registerForInput with a request containing the enableSessionListener node set to true. Then, to receive some message from the standard input (e.g., in( message )) we:

  • define this session's token (e.g., we define a variable token assigning to it a unique value with the new primitive);
  • subscribe our listener with this session's token (subscribeSessionListener@Console( { token = token } )());
  • wait for the data from the prompt (e.g., in( message ));

Finally, when we terminated this session's inputs, we can unsubscribe our listener for this session (unsubscribeSessionListener@Console( { token = token } )());

For a more comprehensive example, we report the code below.

// we define a cset on the InRequest.token node
cset {
    sessionToken: InRequest.token
}

main
{
    test()( res ){
    // we registerForInput, enabling sessionListeners
    registerForInput@Console( { enableSessionListener = true } )()
    // we define this session's token
    token = new
    // we set the sessionToken for the InRequest
    csets.sessionToken = token
    // we subscribe our listener with this session's token
    subscribeSessionListener@Console( { token = token } )()
    // we make sure the print out to the user and the request for input are atomic
    synchronized( inputSession ) {
        println@Console( "insert response data for session " + token + ":" )()
        // we wait for the data from the prompt
        in( res )
    }
    // we unsubscribe our listener for this session before closing
    unsubscribeSessionListener@Console( { token = token } )()
    }
}