The following section will talk about one of the most interesting aspects of the n3rv framework: topologies ! Long story short, topologies are JSON service class proto-definitions. Once loaded (or fetched) by your nodes, the topology tell them:
- which bindings to create.
- which bindings to connect() to.
- which receive callbacks to attach().
- which main loop callbacks to run.
In short, you can dynamically reconfigure an entire n3rv cluster behaviour, that without changing a single line of code nor recompiling your binaries. This is extremely powerful: You can for instance create a design pattern where all your code stands inside a single, “generic” service class, and then instanciate multiple nodes whose behaviour will be controlled through a topology.
As explained above, topologies are JSON files which contain the descriptions of both the external and internal wiring of your nodes.
You will find below an example of JSON topology definition:
{
"topology": [
{
"namespace": "com",
"service_class": "pong",
"binds": [{
"name": "pong",
"port": 11001,
"type": "ZMQ_REP"
}
],
"connects": [],
"receive_callbacks": [
["pong", "ping_received"]
],
"ml_callbacks": [["00_mainloop","mloop"]]
},
{
"namespace": "com",
"service_class": "ping",
"binds": [],
"connects": [{ "uid": "pong_conn",
"lookup": "com.pong.*.pong",
"type": "ZMQ_REQ"} ],
"receive_callbacks": [["pong_conn","pong_received"]],
"ml_callbacks": []
}
]
}
Here we defined 2 service classes: com.ping and com.pong . Now If a node with an uid starting with com.pong loads this topology, it will automatically create a new binding named “pong”, and attach its ping_received callback method to it.
Similarly, if a node with uid starting with com.ping loads this topology, it will automatically try to connect to “com.pong.*.pong” binding, and attach the resulting handler with its pong_received callback.
n3rv offers 2 different ways for nodes to load topologies.
n3rv::service has a dedicated method to load topology files: service::load_topology()
/* We instanciate a new node */
n3rv::service s1("127.0.0.1",10001);
/* We set the node's uid */
s1.set_uid("com.pong.p1");
/** We load topology file and get back the associated qhandlers */
std::map<std::string, qhandler*> qhlist = s1.load_topology("/etc/n3rv/topology.json");
A smarter way is to use the service controller to distribute topologies:
/* We instanciate a new service controller */
n3rv::servicecontroller sc1("0.0.0.0",10001);
/* We tell the sc to load the JSON topology file */
sc1.load_topology("/etc/n3rv/topoplogy.json");
sc1.run_async();
/* We instanciate a new node */
n3rv::service s1("127.0.0.1",10001);
/* We set the node's uid */
s1.set_uid("com.pong.p1");
/* Finally we tell the node to fetch its topology from the service controller */
std::map<std::string, qhandler*> qhlist = s1.fetch_topology();
s1.run();
Note about load_topology() and fetch_topologies() return values: the key string inside the map correspond respectively to the “uid” value of connects objects, and the “name” field of binds objects.
As you may have noticed, receive_callbacks and ml_callbacks contains arrays of string couples. As there is no convenient mechanism in C++ to get a static method pointer given its string name, callbacks ( both receive and main loop ones) need to be “pre-registered” by your service classes, meaning mapped to strings for the topology to find them.
In order to do that, n3rv::service has 2 methods: register_rcb(char*, n3rv::fctptr) and register_mlcb(char*, n3rv::mlptr)
Usually called at the initialize() phase of your service classes, these methods will allow the topology parser to effectively find the callbacks referenced in the JSON.
class example: public n3rv::service {
using n3rv::service::service;
public:
/*let's say that we declare 1 receive callback */
static void* recv(void* objref, zmq::message_t* msg) {}
/* And then 1 Main loop Callback */
static void* ml(void* objref) {}
int initialize() {
/* To make the 2 previously defined callbacks available for topologies,
we need to "preregister" them both: */
this->register_rcb("recv",recv);
this->register_mlcb("ml",ml)
}
};