Sessions

Stateful sessions

Usually a service provides loosely coupled operations, which means that there is no correlation among the different invocations of different operatons. Each invocation can be considered as independent from the others. Neverthless, 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. 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.

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

In this case, we need to route each user message to the right session it is involved in. Such an issue is solved by sending for each message an extra information, usually a session identifier, for correctly routing the message on the server side. We call these kind of information correlation sets.

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 behaviural 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 intepreter looks into the message for finding the node which contains the value to be compared with the correlation values. In the diagram above, the variable x is 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 iniateGame 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;

Then 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.

//interface.iol
type LoginRequest: void {
.name: string
}
type OpMessage: void{
.sid: string
.message?: string
}
interface PrintInterface {
RequestResponse: login(LoginRequest)(OpMessage)
OneWay: print(OpMessage), logout(OpMessage)
}

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.

//server.ol
cset {
sid: OpMessage.sid
}
main
{
login( request )( response ){
username = request.name;
response.sid = csets.sid = new;
// code
}
}

At Lines 3-5 we declare the server's correlation set. 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 OpMessage.sid.

At Line 11, 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
main
{
login@PrintService( request )( response );
opMessage.sid = response.sid;
// if user wants to print
opMessage.message="my Message";
print@PrintService( opMessage );
// else he wants to logout
logout@PrintService( opMessage )
}

Finally, at Line 6, 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 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 )()
}