Programming Microservices with Jolie - Part 1: Data formats, Proxies, and Workflows.
In Microservice architectures, software components are built as reusable and scalable services that interact with each other. While this introduces many advantages, it also brings all the issues of distributed computing to the internals of software applications and developers need to be careful about this. Luckily, we are standing on the shoulders of all the knowledge that we accumulated by using Service-oriented Architectures (SOAs), so we are more aware of these issues and we have more technologies and methodologies to deal with them. Microservices could (and I believe they will) fly after all, in one form or the other, and we at the Jolie team are surely not the only ones sharing this optimism.
But like SOAs were, Microservices are also at risk of exposing programmers to too much complexity to deal with when they take their first steps in this world. And we are at risk again of making arbitrary technology choices that we may regret later, just like SOAP was in many cases.
In developing Jolie, a microservice-oriented programming language, we are trying to make microservice programming simple and productive. A major challenge in this is how we can map design ideas for a microservice architecture to code as fast as possible, without committing to specific implementation details on communications and service deployment/distribution.
The problem of focusing on design issues and not getting locked inside of interoperability problems is particularly dear to me. Jolie helps in this, e.g., by abstracting application logic from the underlying data formats that you will use for communications.
Another problem that is dear to the team in general and I will talk about below is that of making composition of microservices easy. This does not simply mean composition of the operations offered by microservices, but also dealing with how we can deploy new systems based on previously developed systems.
But talk is cheap. Let's do code and see an example.
Ready? Let's proceed.
Open a file and call it calculator.ol.
Then we write:
include "calculator.iol"
execution { concurrent }
inputPort CalcInput {
Location: "socket://localhost:8000"
Protocol: sodep
Interfaces: CalcIface
}
main
{
sum( req )( resp ) {
resp = req.x + req.y
}
}
Above, we are telling Jolie that this microservice is concurrent, i.e., each request is handled in parallel by a lightweight process (Jolie processes are implemented as threads with a local state).
The input port CalcInput states that the service can be accessed using TCP/IP sockets on port 8000, using the SODEP protocol. CalcIface is the exposed interface, which we define in the included file calculator.iol:
type SumT:void {
.x:int
.y:int
}
interface CalcIface {
RequestResponse: sum( SumT )( int )
}
But like SOAs were, Microservices are also at risk of exposing programmers to too much complexity to deal with when they take their first steps in this world. And we are at risk again of making arbitrary technology choices that we may regret later, just like SOAP was in many cases.
In developing Jolie, a microservice-oriented programming language, we are trying to make microservice programming simple and productive. A major challenge in this is how we can map design ideas for a microservice architecture to code as fast as possible, without committing to specific implementation details on communications and service deployment/distribution.
The problem of focusing on design issues and not getting locked inside of interoperability problems is particularly dear to me. Jolie helps in this, e.g., by abstracting application logic from the underlying data formats that you will use for communications.
Another problem that is dear to the team in general and I will talk about below is that of making composition of microservices easy. This does not simply mean composition of the operations offered by microservices, but also dealing with how we can deploy new systems based on previously developed systems.
But talk is cheap. Let's do code and see an example.
Get Jolie
You don't need to have Jolie installed to read this article, but if you want to try what I will show, go install Jolie.Ready? Let's proceed.
A calculator microservice
We are going to program a little toy calculator microservice for making additions. Feeling lazy? Download the code here.Open a file and call it calculator.ol.
Then we write:
include "calculator.iol"
execution { concurrent }
inputPort CalcInput {
Location: "socket://localhost:8000"
Protocol: sodep
Interfaces: CalcIface
}
main
{
sum( req )( resp ) {
resp = req.x + req.y
}
}
Above, we are telling Jolie that this microservice is concurrent, i.e., each request is handled in parallel by a lightweight process (Jolie processes are implemented as threads with a local state).
The input port CalcInput states that the service can be accessed using TCP/IP sockets on port 8000, using the SODEP protocol. CalcIface is the exposed interface, which we define in the included file calculator.iol:
type SumT:void {
.x:int
.y:int
}
interface CalcIface {
RequestResponse: sum( SumT )( int )
}
CalcIface is an interface with only one Request-Response operation called sum, which receives a tree with two nodes (x and y, both integers) and returns an integer.
The sum operation is implemented in the main procedure of the calculator microservice: it simply receives the message on variable req and replies with the sum of the two subnodes x and y to the invoker.
Using the calculator
Using the calculator service from Jolie is easy. We create a client.ol program, include the interface and off we go:
include "calculator.iol"
include "console.iol"
outputPort Calculator {
Location: "socket://localhost:8000"
Protocol: sodep
Interfaces: CalcIface
}
main
{
with( req ) {
.x = 3;
.y = 2
};
sum@Calculator( req )( resp );
println@Console( resp )()
}
Running this client program will reach the calculator service and print the number 5 on screen. SODEP is a binary protocol, so you don't need to worry about overhead as much as you have to in, e.g., SOAP.
The attentive reader may have noticed that in our client code we are calling the println operation of the Console service, just like we are calling the sum operation of calculator. That's right, even our Console library is a microservice that you can relocate as you like.
OK, but what if the rest of my system is not in Jolie?
Jolie is interoperable with many other technologies, both server- and client-side. Let's see what happens if our client is not written in Jolie and does not support SODEP. Let's say instead that it's a web browser and that it consequently uses HTTP.
Take the calculator program again and add the following before the main procedure:
inputPort CalcWebInput {
Location: "socket://localhost:8080"
Protocol: http
Interfaces: CalcIface
}
Et voilà, now calculator is not only exposed on TCP port 8000 with SODEP but also on TCP port 8080 with the HTTP protocol. Run calculator and you can now point your browser to:
You will see calculator crunching the numbers and reply with:
You can use many other protocols (HTTP/JSON, SOAP, local memory, ...) and communication mediums (Bluetooth, UNIX sockets, ...), depending on your needs. See here.
Location: "socket://localhost:8080"
Protocol: http
Interfaces: CalcIface
}
Et voilà, now calculator is not only exposed on TCP port 8000 with SODEP but also on TCP port 8080 with the HTTP protocol. Run calculator and you can now point your browser to:
You will see calculator crunching the numbers and reply with:
<sumResponse>8</sumResponse>
You can use many other protocols (HTTP/JSON, SOAP, local memory, ...) and communication mediums (Bluetooth, UNIX sockets, ...), depending on your needs. See here.
What if I cannot change the code of my microservices?
If your calculator is a black box that you cannot or do not want to touch, then you cannot just edit its code to add an HTTP input port. This is the typical case in which you would like to compositionally evolve your system without touching what has already been deployed.
The standard solution is to use a proxy. In our example, we wish to make a proxy that enables web browsers to communicate with the calculator service:
Web browser <-> Proxy <-> Calculator->->
Proxies (and variants) are very useful in general as intermediate glue components in service systems, but managing them may require you getting to know yet another technology. In Jolie, instead, proxies are powered by a native language primitive for forwarding messages and integrate well with our communication ports, so you can take advantage of the data abstraction layer provided by Jolie.
Open a file proxy.ol and write:
include "calculator.iol"
include "console.iol"
execution { concurrent }
outputPort Calculator {
Location: "socket://localhost:8000"
Protocol: sodep
Interfaces: CalcIface
}
inputPort ProxyInput {
Location: "socket://localhost:9000"
Protocol: http
Aggregates: Calculator
}
main
{
in()
}
The important part here is that we have an input port on TCP port 9000 that aggregates the output port towards the Calculator. This means that all messages on port ProxyInput for an operation provided by Calculator will be forwarded to that service. So now we have our proxy and we can navigate to:
The proxy service will now receive your HTTP message on port ProxyInput, see that it is for the sum operation of calculator. The message is therefore converted to the SODEP format, as specified by the output port towards the calculator. The response from calculator will finally be converted again from SODEP to HTTP and sent to the initial client.
Aggregation is really useful. You can also add hooks (called couriers) for intercepting messages and, e.g, log operations or check for authentication credentials.
Workflows
Alright, now we have our toy microservice system. Let's say that after a while, you want to add the requirement that a user must log in before calling the sum operation on the calculator. This is a workflow, the magic keyword being before in the previous sentence. Jolie inherits native workflow primitives from business process languages (e.g., BPEL), making handling flows of communications among services a breeze!
Open calculator.ol and change the main definition as follows:
cset {
sid : SumT.sid
}
main
{
login( creds )( csets.sid ) {
if ( creds.pwd != "jolie" ) {
throw( InvalidPwd, "The password is jolie" )
};
csets.sid = new
};
sum( req )( resp ) {
resp = req.x + req.y
}
}
The cset block above declares a session identifier sid, which we will use to track invocations coming from a same client (that's a correlation set). We also introduced a new operation, login, which simply checks if the password sent by the client is jolie and otherwise throws a fault to the invoker. Observe now that we use the semicolon operator to tell Jolie that sum becomes available only after login has successfully completed.
This is a fundamental difference with respect to, say, object-oriented computing, where if you want to enforce order in how different methods are called, you have to implement it yourself with bookkeeping variables and concurrent lock mechanisms. Here instead, you just write a semicolon.
We also need to update the interface of the calculator, in calculator.iol, to account for our modifications:
type SumT:void {
.x:int
.y:int
.sid:string
}
type LoginT:void {
.pwd:string
}
interface CalcIface {
RequestResponse:
login( LoginT )( string ) throws InvalidPwd( string ),
sum( SumT )( int )
}
We are done! No need to update the code of the proxy, as the aggregation primitive is parametric on the interface and the protocols used by the aggregated services. Jolie will do that lifting for us. So now we can browse to:
You will get a random session identifier. I got:
<loginResponse>193e6a7a-895c-4ff6-b32a-5bbab0eea7f3</loginResponse>
So now I can go to (remember to replace sid with what you got in the previous call if you try this yourself):
And receive my good old 8 as result.
Have a look at the Jolie documentation if you are interested in using cookies and theming your responses with nice HTML. You can put that in a separate microservice, to keep calculator clean!
Conclusions
This is just the tip of the iceberg of what you may have to do with microservices and what Jolie can do for you.
But we can already see some of the underlying concepts that Jolie is based on:
- You should not commit to any particular communication technology. Microservices are born to be lean and reusable, and should be kept decoupled from the implementation details of how data is exchanged.
- Building proxies and other kinds of architectural composers is very handy in microservice architectures just like it is in any other distributed system. However, with microservices you can end up having a lot of them. Jolie helps in creating and managing these composers by making them programmable with native primitives that make it clear what is happening.
- Workflows can help in making the structure of communications followed by a service explicit. They should be easy to write and Jolie strives in doing so. See here for our other workflow primitives.
Stay tuned for the next chapters. :-)