Dynamic Binding
Dynamic binding
Jolie allows output ports to be dynamically bound, i.e., their locations and protocols (called binding informations) can change at runtime. Such a feature is very important because it allows for the creation of dynamic systems where components (microservices) can be bound at runtime.
Dynamic binding in Jolie
Changes to the binding information of an output port is local to a session: the configuration of the output ports are considered part of the local state of each session, indeed. Dynamic binding is obtained by treating output ports as variables. As an example, let us consider this example, where there is a printer-manager that is in charge to forward a message to print, to the right target service depending on the printer selection of the client. There are two printer services available: printer1
and printer2
. Both of them share the same interface.
In particular the code of the printer-manager follows:
from PrinterInterface import PrinterInterface
from PrinterManagerInterface import PrinterManagerInterface
service PrinterManager {
execution: concurrent
outputPort Printer {
protocol: sodep
interfaces: PrinterInterface
}
inputPort PrinterManager {
location: "socket://localhost:8000"
protocol: sodep
interfaces: PrinterManagerInterface
}
main {
print( request )( response ) {
if ( request.printer == "printer1" ) {
Printer.location = "socket://localhost:8001"
} else if ( request.printer == "printer2" ) {
Printer.location = "socket://localhost:8002"
}
printText@Printer( { text = request.text } )()
}
}
}
Here the dynamic binding is simply obtained by using a variable assignment.
if ( request.printer == "printer1" ) {
Printer.location = "socket://localhost:8001"
} else if ( request.printer == "printer2" ) {
Printer.location = "socket://localhost:8002"
}
Note that the location of port Printer
can be simply overwritten by referring to it using the path Printer.location
.
Example: programming a chat
We show a usage example of dynamic binding and binding transmission by implementing a simple chat scenario. It is composed by a chat-registry
which is in charge to manage all the open chats and participants, and a user
service which is in charge to manage a single participant connected to a chat. There are no limits to the users that can be connected to a chat. In the following diagram we report an example of the architecture where three users are connected to the service chat-registry
.
The code can be consulted at this link.
The chat-registry
and each user
service exhibit an inputPort for receiving messages. The outputPort of the chat-registry
which points to users is not bound to any service, but it needs to be bound dynamically depending on the users connected to a chat.
The chat-registry
offers two operations: addChat and sendMessage. The former operation permits to a user to connect to a chat, whereas the latter is exploited by the user to send messages to all the participants of a chat. The user
service is composed by two services: service User
, which is the main one, and service UserListener
which is embedded by the former. Service UserListener
is in charge to receive messages from the chat-registry
whereas the latter just manages the console for enabling human interactions and sending local messages to the chat-registry
.
Dynamic binding is exploited in the implementation of the sendMessage operation of the chat-registry
where every time a message is received the user's outputPort is bound to each registered user for forwarding messages. Note that user's locations are stored into the hashmap global.chat.\( \).users.\( \).location
which is set every time a user requests to be connected to a chat by using operation addChat.
[ sendMessage( request )( response ) {
/* validate token */
if ( !is_defined( global.tokens.( request.token ) ) ) {
throw( TokenNotValid )
}
}] {
/* sending messages to all participants using dynamic binding */
chat_name = global.tokens.( request.token ).chat_name
foreach( u : global.chat.( chat_name ).users ) {
/* output port dynamic rebinding */
User.location = global.chat.( chat_name ).users.( u ).location
/* message sending */
if ( u != global.tokens.( request.token ).username ) {
msg << {
message = request.message
chat_name = chat_name
username = global.tokens.( request.token ).username
}
scope( sending_msg ) {
install( IOException =>
target_token = global.chat.( chat_name ).users.( u ).token
undef( global.tokens.( target_token ) )
undef( global.chat.( chat_name ).users.( u ) )
println@Console("User service not found, removed user " + u + " from chat " + chat_name )()
)
setMessage@User( msg )
}
}
}
}
The operation setMessage is exploited by the chat-registry
to send a message to each participant of the chat. Note that such an operation is exhibited in the inputPort of the service UserListener
at the user side.
Compatibility of the interfaces
It is worth noting that, in case of dynamic binding, the interfaces defined in the output port must be compatible with those defined into the receiving input port. The following rules must be respected for stating that there is compatibility between two interfaces:
- all the operations defined in the interfaces at the output ports must be declared also in the interfaces at the input port (it does not matter in which interface an operation is defined, it is important that it is defined).
- all the types of the messages defined for the operations of the output port, must be compatible with the the correspondent type of the same operation at the receiving input port.
- a sending message type is considered compatible with the correspondent receiving one, when all the message it represents can be received without producing a TypeMismatch on the receiver part.