Ballerina Connectors/Endpoints and What? How? Why?

Rajith Vitharana
Ballerina Swan Lake Tech Blog
5 min readNov 8, 2017

Ballerina is a general purpose programming language which is being developed by WSO2, More details in “https://ballerinalang.org/

Ballerina has two major concepts to interact with outside world, Server connectors and Client connectors. This story is about Ballerina Client connectors. (Will create a separate story about Ballerina Server connectors)

Ballerina connectors use actions to interact with outside world. For example below is a sample Ballerina connector definition.

public connector HttpClient(string url) {
action post(string path, Request req)(Response) {
.....
.....
return resp;
}
}

So how to use above connector to interact with external world? Here comes the concept called “endpoint”. In ballerina, Endpoints are logical entities that represent remote services (e.g: HTTP service, database etc). These endpoints should have connections that are associated with them to do required communications. So now comes the questions, what is a connection? and how can we create a connection in Ballerina? Simple answer is you had the answer in the question itself :) We can create connections using “create” keyword in conjunction with connectors. Syntax looks like follows.

HttpClient client = create HttpClient("http://localhost:9090");

So now, how do we associate this connection with an endpoint? This can be done in two ways, one is initializing an endpoint with connection in a single go. Following is the syntax for that.

endpoint<HttpClient> ep {
create HttpClient("http://localhost:9090");
}

Other way would be to bind a new connection with an existing endpoint. This also has a special syntax as follows.

bind connection with endpoint;

So for example if we need to bind above “client” connection with “ep” endpoint, then that would look like,

bind client with ep;

Last part is how we invoke actions. Now we have the endpoint ep created, we can invoke actions on top of that with dot notation. Below is how you do that with Ballerina.

Request req = {msg:"hello world"};
ep.post("/hello", req);

So now you know the basics and syntaxes about connectors and endpoints. :)

Let’s dig deep into some concepts.

The whole idea of above design is to allow developers to build their own connectors and let them link/chain them in any order they like. What does this mean?

If we take a simple example scenario to explain this. Say there is a HttpClient connector which can be used to connect with http services. Then after some time we comes across a requirement to support basic auth with that HttpClient connector. Solution would be to extend this connector to support Basic Auth. One way of extending this is integrate basic auth related logic in to the same HttpClient connector. But as you can imagine, that’s not a scalable solution, because if we are going to integrate logics for each and every requirement we comes across to the same connector, then that connector will pretty soon become very bulky and complex and ugly. So what is the best way to achieve this?

We can simply write another connector(BasicAuth connector) which takes a HttpClient connector as a parameter. Along with that parameter, BasicAuth connector will take other required parameters for the functionalities it is going to perform(such as user name and password). Below is the sample implementation.

public connector BasicAuth(HttpClient c, string usr, string pass){
action post(string path, Request req)(Response){
endpoint<HttpClient> ep {
c;
}
string enc = util:base64Encode(usr + ":" + pass);
req.setHeader("Authorization", "Basic " + enc);
return c.post(path, req);
}
}

As you can see, what BasicAuth connector do is set the required basic auth headers to the request and invoke the endpoint using the provided HttpClient connection.

for the sake of understanding the power of these concepts easily let’s create another extended connector called RetryCon, which will simply retry the request in case of failure.

public connector RetryCon(HttpClient c, int count, int rest){
action post(string path, Request req)(Response){
endpoint<HttpClient> ep {
c;
}
int i = 0;
while(i < count) {
try {
return ep.post(path, req);
} catch (error e) {
sleep(rest);
print("retrying, count - " + i);
}
}
error er = {msg:"cannot post data"}
throw er;
}
}

I think the code is self explanatory, anyway if I explain it bit more, what above connector does is retry the request until it succeed or it hits the given retry limit. If a retry fails, it will wait the given “rest” time before retrying the request again.

So now, what can we achieve with above connectors? simply retry or basic auth for a request? that’s definitely possible, but how about basic auth with retry? yes it’s also possible :)

So how does Ballerina support this?

The main functionality Ballerina provide to facilitate this chaining behavior is that Ballerina supports connector casting. Simply put, you can cast and use one connector instead of another. So does it mean that we can cast any connector to any other connector? No, (that will be useless because in that case we don’t know which action to invoke).

Ballerina enforce only single restriction against this casting. That is, those connectors should be compatible. What does it mean by compatible connectors? It means, both connectors should have same action signatures. That’s awesome right :) you can cast any connector to any other connector if both have matching actions. No need to extend anything, no need to implement anything.

You may have already wondered why I’m using same action signatures in the sample connectors I was creating. Well now you know :) (There is no such restrictions as to have matching actions in connectors, only thing is if you don’t have matching actions, then you won’t be able to cast one connector to another hence you won’t be able to chain them in different ways.)

So let’s see how to build a retrying basic auth http client using above connectors. (I will break this to multiple lines so that it is easily understandable)

HttpClient client = create HttpClient("http://localhost:9090");
BasicAuth basic = create BasicAuth(client, "abc", "123");
RetryCon retryCon = create RetryCon((BasicAuth) basic, 5, 2000);

Then you can use above created “retryCon” inside an endpoint to do retrying basic auth requests.

I’ll do the full story in a single example

function main (string[] args) {
endpoint<HttpClient> ep {
create RetryCon((BasicAuth) create BasicAuth(create HttpClient(), "abc", "123"), 5, 2000);
}
Request req = {msg:"hello world"};
Response resp = ep.post("/hello", req);
println(resp.getStringPayload());
}

If you have noticed in above code, even thought I’m using an endpoint with type “HttpClient” and creating a connector of “RetryCon” type for that, I haven’t used any casting syntax there. :) that’s not a mistake, endpoints are type compatible, that is, you can use any compatible connector(compatible with endpoint constraint type) inside an endpoint, no need for casting.

That explains the full story behind the design of Ballerina endpoint and connector implementation.

Hope this helps in understanding those concepts and thought process behind those design decisions :)

--

--