Scopes and faults
Scopes
A scope
is a behavioural container denoted by a unique name and able to manage faults. Remarkably, in a service behaviour, the main
is a scope named main. We say that a scope terminates successfully if it does not raise any fault signal; a scope obtains this by handling all the faults thrown by its internal behaviour.
The primitive throw
A fault is a signal, identified by its name, raised by a behaviour towards the enclosing scope when an error state is reached, in order to allow its recovery.
Jolie provides the statement throw
to raise faults.
Scope and throw syntax follows.
scope( scope_name )
{
// omitted code
throw( FaultName )
}
Fault handlers, the primitive install
The install
statement provides the installation of dynamic fault handlers within a scope. The primitive install
joins a fault to a process and its handler is executed when the scope catches the fault.
scope( scope_name )
{
install (
Error1 => // fault handling code,
...
ErrorN => // fault handling code
);
// omitted code
throw( fault_name )
}
A fault which is not caught within a scope, is automatically re-thrown to the parent scope. In the following example whose runnable code can be found here, a simple jolie script asks the user to insert a number, if the number does not correspond to the secret
one, a fault is raised.
include "console.iol"
main
{
registerForInput@Console()();
install( WrongNumberFault =>
/* this fault handler will be executed last */
println@Console( "A wrong number has been inserted!" )()
);
/* number to guess */
secret = 3;
scope( num_scope )
{
install( WrongNumberFault =>
/* this fault handler will be executed first, then the fault will be re-thrown */
println@Console( "Wrong!" )();
/* the fault will be re-thrown here */
throw( WrongNumberFault )
);
print@Console( "Insert a number: " )();
in( number );
if ( number == secret ) {
println@Console("OK!")()
} else {
/* here the fault is thrown */
throw( WrongNumberFault )
}
}
}
It is worth noting that the fault is firstly caught by the first handler defined within the scope num_scope which will execute the following code:
println@Console( "Wrong!" )();
throw( WrongNumberFault )
It will print the string "Wrong!"
in the console and then it will re-throw the fault to the parent scope, the scope main. At this point the second fault handler defined at the level of the main scope will be executed:
println@Console( "A wrong number has been inserted!" )()
Install statement priority
An install statement may execute in parallel to other behaviours that may throw a fault. This introduces a problem of nondeterminism: how can the programmer ensure that the correct handlers are installed regardless of the scheduling of the parallel activities? Jolie solves this issue by giving priority to the install primitive with relation to the fault processing, making handler installation predictable.
As an example, consider the following code which can be found also here:
scope( s )
{
throw( f ) | install( f => println@Console( "Fault caught!" )() )
}
where, inside the scope s
, we have a parallel composition of a throw
statement for fault f
and an installation of a handler for the same fault. The priority given to the install primitive guarantees that the handler will be installed before the fault signal for f
reaches the scope construct and its handler is searched for.
RequestResponse Pattern and transmission of data into a fault
Uncaught fault signals in a request-response body are automatically sent to the invoker. Hence, invokers are always notified of unhandled faults. We introduced the syntax for declaring a fault into an interface at Section Interfaces
Here we transform the previous example script into a service in order to introduce a request-response operation (operation guess). You can find the complete code here
type NumberExceptionType: void{
.number: int
.exceptionMessage: string
}
interface GuessInterface {
RequestResponse: guess( int )( string ) throws NumberException( NumberExceptionType )
}
The interface defines the operation guess
able to throw a NumberException
, whose message type is NumberExceptionType
.
include "GuessInterface.iol"
include "console.iol"
execution{ concurrent }
inputPort Guess {
Protocol: sodep
Location: "socket://localhost:2000"
Interfaces: GuessInterface
}
init {
secret = int(args[0]);
install( FaultMain =>
println@Console( "A wrong number has been sent!" )()
);
install( NumberException =>
println@Console( "Wrong number sent!" )();
throw( FaultMain )
)
}
main
{
guess( number )( response ){
if ( number == secret ) {
println@Console( "Number guessed!" )();
response = "You won!"
} else {
with( exceptionMessage ){
.number = number;
.exceptionMessage = "Wrong number, better luck next time!"
};
/* here the throw also attach the exceptionMessage variable to the fault */
throw( NumberException, exceptionMessage )
}
}
}
The server implements the throw statement in the else branch of operation guess
behaviour. If the number sent by the client is different than the secret
one, the request-response operation will send a NumberException
fault to the client along the fault data.
Joining data to a fault
The syntax for joining data into a fault is a simple extension of the throw
syntax given previously.
scope ( scope_name )
{
install ( FaultName => /* fault handling code */ );
// omitted code
throw ( FaultName, faultData )
}
In the server of the example above, it is obtained by the following piece of code:
with( exceptionMessage ){
.number = number;
.exceptionMessage = "Wrong number, better luck next time!"
};
throw( NumberException, exceptionMessage )
Let us check the client of the example in order to show how it handles the raise of the fault and prints the data sent from it:
main
{
install( NumberException=>
println@Console( main.NumberException.exceptionMessage )()
);
guess@Guess( 12 )( response );
println@Console( response )()
}
It is worth noting that, in order to correctly reference fault data within a fault handler, it is necessary to specify the scope path where the fault is contained. The path is always built in the following way:
- name of the scope.Name of the fault.Node of the message to access
Thus in the example, since we want to access the node exceptionMessage
, we use the following path:
main.NumberException.exceptionMessage )()
Accessing a fault caught in a scope: the alias default
In some cases, we do not want to specify all the handlers of all the faults raised within a scope, but we want to specify a unique handler for all those faults without a handler. In this case it is possible to check if scopes caught faults and also to access the contents of faults thanks to alias default
.
With syntax scope_name.default
we access the name of the fault caught by the scope.
Used in combination with dynamic lookup, with syntax scope_name( scope_name.default ).faultMessage
, we can access the message sent with the fault, for instance msg
in the example below.
scope ( s ){
install( MyFault =>
println@Console( "Caught MyFault, message: " + s.MyFault.msg )()
);
faultMsg.msg = "This is all MyFault!";
throw( MyFault, faultMsg )
};
println@Console( "Fault message from scope s: " + s.( s.default ).msg )()