Embedding
Embedding is a mechanism for launching a service from within another service. A service, called embedder, can embed another service, called embedded service, by targeting it with the embed
primitive.
The syntax for embedding is:
embed serviceName( passingValue ) [in existedPortName | as newPortName]
Above, we see that the embed
statement takes as input a service name and an optional value.
Then, we can optionally bind an inputPort of the embedded service (which must be set as local) to an outputPort of the embedder.
To achieve that, we have two modalities:
- using the
in
keyword we bind the inputPort of the target to an existing outputPort defined by the embedder; - using the
as
keyword we create a new outputPort that has the same interface of the inputPort of the embedded service, besides being bound to it.
When that trailing part is missing, the embedded service runs without any automatic binding — however that does not mean it is not callable in other ways, e.g., through a fixed TCP/IP address like "socket://localhost:8080"
or though a local location like "local://A"
).
Embedding the standard library
The Jolie standard library comes as a collection of services that users can import
and use through the embedding mechanism. The usage of statement import
is documented in section Modules
The following example shows the usage of the Console
service, which exposes operations for communication between Jolie and the standard input/output:
from console import Console
service MyService {
embed Console as C
main {
print@C( "Hello world!" )()
}
}
The section of the documentation dedicated to the standard library reports more information on the modules, types, and interfaces available to programmers with the standard Jolie installation.
An example of embedding
Embedding Jolie services is very simple. In order to show how it works, let us consider a simple example whose executable code can be found here.
In this example we want to implement a service which is able to clean a html string from the tags <div>
, </div>
, <br>
and </br>
replacing br
with a line feed and a carriage return. In order to do this, we implement a parent service called clean_div.ol which is in charge to clean the div tags and another service called clean_br.ol in charge to clean the br tags. The service clean_div.ol embeds the service clean_br.ol:
from CleanBrInterface import CleanBrInterface
from CleanDivInterface import CleanDivInterface
from string_utils import StringUtils
from clean_br import CleanBr
service CleanDiv {
execution: concurrent
outputPort CleanBr {
Interfaces: CleanBrInterface
}
embed StringUtils as StringUtils
embed CleanBr in CleanBr
inputPort CleanDiv {
Location: "socket://localhost:9000"
Protocol: sodep
Interfaces: CleanDivInterface
}
main {
cleanDiv( request )( response ) {
replaceAll@StringUtils( request { regex="<div>", replacement="" })( request )
replaceAll@StringUtils( request { regex="</div>", replacement="\n" })( request )
cleanBr@CleanBr( request )( response )
}
}
}
It is worth noting that:
- it is necessary to import the service CleanBr from module clean_br.ol and its interface from module CleanBrInterface
- the outputPorts CleanBr and StringUtils do not define nor the location and the protocol because they are set by the embedding primitive with location "local" and inner memory protocol.
- the embedding primitive joins the service clean_br.ol with the outputPort CleanBr thus implying that each time we use port CleanBr inside the behaviour we will invoke the embedded service:
cleanBr@CleanBr( request )( response )
- the StringUtils is a service embedded from the standard library
Hiding connections
Note that the embedding primitive, together with the usage of in-memory communication, allows for hiding connections among embedded microservices. In the example above the connection between the service clean_div.ol and clean_br.ol is hidden by the embedding and no external microservices can call the inputPort of the microservice clean_br.ol.
Cells (or multi services)
Here we introduce the concept of cells as a unique execution context for a set of services. In a cell, one or more services can be executed within the same execution context. When there is only one service, the definition of a cell corresponds to the same of a service. A cell exhibits only the public available ports of the inner services. The ports that are not reachable by external invokers are considered internal ports and they are hidden from the point of view of a cell. Operationally, a cell can be obtained by exploiting the embedding primitive.
Creating a script from a service architecture
Afterwards, we can write a modified version of the client program of the previous example, in order to directly embed the service clean_dv.ol thus transforming the service architecture into a single script. The code of this example can be found here. Here we report the code of the script:
from CleanDivInterface import CleanDivInterface
from console import Console
from clean_div import CleanDiv
service Script {
outputPort CleanDiv {
Interfaces: CleanDivInterface
}
embed CleanDiv in CleanDiv
embed Console as Console
main
{
div = "<div>This is an example of embedding<br>try to run the encoding_div.ol<br>and watch the result.</div>"
println@Console("String to be cleaned:" + div )()
cleanDiv@CleanDiv( div )( clean_string )
println@Console()()
println@Console( "String cleaned:" + clean_string )()
}
}
It is worth noting that now the file script.ol embeds the service clean_div.ol which embeds the service clean_br.ol. Since script.ol does not implement any inputPort but it just executes a script, when it reach the ends all the embedded services are automatically shut down.
Dynamic Embedding
Dynamic embedding makes possible to associate a unique embedded instance to a single process of the embedder, thus allowing only that specific process to invoke the operations of the embedded service. Such a feature can be obtained by exploiting the API of the runtime service, in particular we will use operation loadEmbeddedService.
As an example let us consider the case of a calculator which offers the four arithmetic operators but it loads the implementation of them at runtime depending on the request selection. If the client specifies to perform a sum, the calculator will load the service which implements the sum and it will call it on the same operation run. The full code of the example can be found here, in the following we report the code of the calculator:
from runtime import Runtime
from .OperationInterface import OperationInterface
from .CalculatorInterface import CalculatorInterface
service Calculator {
execution: concurrent
embed Runtime as Runtime
/* common interface of the embedded services */
outputPort Operation {
interfaces: OperationInterface
}
inputPort Calculator {
location: "socket://localhost:8000"
protocol: sodep
interfaces: CalculatorInterface
}
/* this is the body of each service which embeds the jolie service that corresponds to the name of the operation */
define __embed_service {
emb << {
filepath = __op + ".ol"
type = "Jolie"
};
/* this is the Runtime service operation for dynamic embed files */
loadEmbeddedService@Runtime( emb )( Operation.location )
/* once embedded we call the run operation */
run@Operation( request )( response )
}
/* note that the embedded service is running once ofr each enabled session then it expires.
Thus each call produce a new specific embedding for that call */
main {
[ sum( request )( response ) {
__op = "sum";
/* here we call the define __embed_service where the variable __p is the name of the operation */
__embed_service
}]
[ mul( request )( response ) {
__op = "mul";
__embed_service
}]
[ div( request )( response ) {
__op = "div";
__embed_service
}]
[ sub( request )( response ) {
__op = "sub";
__embed_service
}]
}
}
The definition embed_service it is called within each operation offered by the calculator (sum, sub, mul, div) and it loads the specific service dynamically. Note that in this example the service files are named with the same name of the operations, thus there are the following files in the same folder of calculator.ol: sum.ol, sub.ol, mul.ol, div.ol. Each of them implement the same interface with different logics, thus they are polymorphic with respect the following interface:
type RequestType {
x: double
y: double
}
interface OperationInterface {
RequestResponse:
run( RequestType )( double )
}
It is worth noting that the embedded service is running once for each enabled session then it expires. Thus each call produce a new specific embedding for that call.