The following example use cases shows how to use mBrane API.
One mBrane Node object is instantiated on each computer participating in the application cluster. A Node is meant to run as a Daemon and should automatically restart when terminated abnormally. Modules connected to a Node should handle a Node restart without any impact on the user code. The maximum downtime of a Node should be less than N milliseconds. Nodes can either auto-discover each other or a new node can be specifically told about an existing network node. A node can join a network, leave a network, restart and terminate.
Modules run separately from the local Node and communicates directly to the Node using shared memory. If a node is not running, the module will wait for N milliseconds for a Node to be instantiated, before exiting with an error. Once a Module is connected to the Node it will register itself on the network with name and properties. If the local Node is restarted the module will re-register automatically.
Once a module has registered it can submit its subscription, if needed. A subscription may already exist for the module, and this is overwritten each time the module submits a new one. At startup and after each subscription the module will read and cache its own current subscription from the network. If another module changes the subscription the new subscription will be received as a notification and cached locally. If the local Node is restarted the module will re-submit the last cached subscription.
A Message is a placeholder for data and will be instantiated by the sending Module. The Module will fill in the fields in the Message and post it. The Node will determine who has subscribed for this Message and forward a copy of the Message either to both locally connected receivers and receivers connected to other Nodes in the system. If possible, the original Message object will be reused for one of the local receivers to avoid unnecessary memory allocation.
Messages posted to Streams are identical in nature to the non-streaming Messages, except for the fact that they contain a 32-bit unsigned integer sequence number.
A signal is used for notification of events, such as Module Created, Subscription changed, etc. Users can also define their own application specific signal types. A Module can subscribe to signal notifications if they are interested in one or more signal events.
No information needs to be provided to the Node object at initialisation time:
MBNode* node = new MBNode(); (called by mBrane::main())
There are two ways that a Node can join a network:
* Automatic - using UDP to discover other Nodes on the network
bool res = node→joinNetwork();
* Manually - provide an address and optionally a port of one other node in the network
bool res = node→joinNetwork(“server.com”);
bool res = node→joinNetwork(“server.com”, 12445);
bool res = node→leaveNetwork();
bool res = node→leaveNetwork();
if (!res) error;
delete(node);
node = new MBNode();
bool res = node→joinNetwork();
bool res = node→leaveNetwork();
if (!res) error;
delete(node);
Once the Node Network has been established, any node can act as the Test Master Node:
* Unit Test - automatically uses all available nodes in network
mBrane::TestResult *result=mBrane::Test::AParticularUnitTest::new(mBrane::Node::Get())→run();
in addition we'll have to test the node functions with respect to module execution (ex: order of delivery, prioritization, etc): this calls for building test modules and run them in coordination with unit tests
* Performance Test - automatically uses all available nodes in network
mBrane::TestResult *result=mBrane::Test::AParticularPerformanceTest::new(mBrane::Node::Get())→run();
for intrinsic capabilities (networking, RAM allocation, latencies, etc). In addition, for extrinsic capabilities (performance under application load) we could use entities to build organizations of test modules to stress the system in various situations
Generally speaking, testing / profiling shall combine unit tests / profiles and system-wide stress configurations. In other words, unit testing / profiling must describe the capacity of the system to bear load / complexity (like messaging patterns).
For more information about testing, see testing_and_performance.
Since the Node should already be running on the local computer a module can be initialised using
MBModule* module = new MBModule(“MyName”); mBrane::Module *m=new CommonLib::C();m→init(parameters);
since modules are class instances, where C is the module class, loaded in the CommonLib, and parameters come from the relevant psyspec file
If the user wants to wait forever for the Node to be ready or be started:
bool res = module→register();
If the user wants to wait for N milliseconds for the Node to be ready or be started and after this give up:
bool res = module→register(N);
[E: I'd prefer a node to broadcast a “ready” signal upon which modules could react, registration being enacted in at module instantiation time by the node. NB: a module is executed by a holding thread controlled by the node. If the node is down or not ready no thread is running and the module cannot be executed.] [T: How would that look, then?]
To query the local Node, again with a timeout:
Node *n=mBrane::Node::Get(); …; n→register(this);…; mBrane::Node::Descriptor *d=n→getDescriptor();
This could be done by invoking a callback when the node state changes instead of queries. The goal is to inverse the control flow, as needed by the many-modules-scheduled-and-executed-by-one-thread arrangement.
As part of the registration the current subscription will be downloaded to the module, if the network knows of a subscription already, either because
* the module was connected and got or chose to be disconnected * another module entered a subscription for the module
This can be seen using
int count = module→getSubscriptionCount();
MBSubscription* sub = module→getSubscription(id);
[E: we should also introduce subscription sets. Apparently the subscriptions you describe here stand for the subscription to one message class|content id. It would be useful for a module to be able to compare different sets coming from different states (previous subscriptions, subscriptions made by other modules, etc. Then only one set has to be published.] [T: I like that! How would that look…?]
A subscription can be registered using subscription objects.
MBSubscription* sub = new MBSubscription();
sub→subscribeType(“Type”); [E: what is type? why parameters are still a strings?]
sub→subscribeFrom(“Module”);
sub→subscribeTo(“Module”);
sub→subscribeAfter(timeout);
sub→subscribeContent(“StringMatch”);
[E: what does it mean?]
[T: This was for the option of local filtering by content, ie. messages containing “*mystring*” - is that possible or doable?]
int id = module→addSubscription(sub);
int count = module→getSubscriptionCount();
MBSubscription* sub = module→getSubscription(id);
bool res = module→removeSubscription(id);
bool res = module→publishSubscriptions();
bool res = module→publishSubscriptions();
bool res = module→removeAllSubscriptions();
bool res = module→publishSubscriptions();
If the user wants to disconnect from the network
mBrane::Node::Get()→unregister(this);
This will inactivate, but not delete the current subscriptions. These will be reactivated if the module connects again.
delete(module); when the crank returns a termination value
mBrane::Node::Get()→terminate(this); when the termination decision is made from a user-defined thread spawn from the module
When a module is registered on the network it can post messages:
MBMessage* message mBrane::Node::get()→message(new CommonLib::C(args));
bool res = message→setTo(“Module”);
bool res = message→setType(“Type”); [T: Do we need this?]
bool res = message→setCC(“Module”); [T: Do we support this?]
bool res = message→setContent(char*, len);
bool res = mBrane::Node::get()→post(message);
Two modes:
* Callback Crank
namespace CommonLib{
int MyModuleClass::MyCrank(Module *mod,Message *msg){
return ( (MyModuleClass *) mod)→myCrank(msg);
}
int MyModuleClass::myCrank(Message *msg){
(user code)
return 0;
}
}
Where the address of MyCrank is stored in a static array in MyModuleClass, indexed by the crank ID. All of these are built and initialized at CommonLib compilation / loading time, from the module class declaration and definition. In particular the code for MyCrank is generated, by macros / template instantiation; same for static data like class and crank IDs. NB: with the inverted control flow, modules are always running in continuous mode.
* Continuous Crank
int MyCrankFunction(MBModule* module) {
if (module == NULL) return -1;
MBMessage* message;
int timeout = 100;
while (some condition) {
message = waitForTriggerMessage(timeout);
if (message != NULL) {…}
}
return 0;
}
Alternative getTriggerMessage calls:
Get the Nth trigger message instead of the first:
MBMessage* message = getTriggerMessage(N);
If N not provided, default value is 0;
Wait for N milliseconds for a new trigger message to appear:
MBMessage* message = waitForTriggerMessage(N);
If a trigger message is already there, this acts like getTriggerMessage();
mBrane Streams do not have any storage and basically work very much like normal messaging. If the module has subscribed to a stream called S it will be triggered by stream messages just like normal messages, probably just at a higher frequency, so the code should expect this and act accordingly to prevent exponential queuing.
If a module is subscribed to a stream S it can post to it by posting messages of type S: [E: I don't understand “posting to a stream”.] [T: Output data to the stream S]
MBMessage* message = new MBMessage();
bool res = message→setType(“S”);
bool res = message→setContent(char*, len);
bool res = module→post(message);
When a module is registered on the network it can post custom signals:
MBSignal* signal = mBrane::Node::Get()→signal(new CommonLib::C(args));
bool res = signal→setType(“Z”);
bool res = module→post(signal);
where C is the message class. NB: nothing differentiates a message from a signal. Only its handling by a node is different.
mBrane Signals work very much like normal messaging. If the module has subscribed to a signal called Z it will be triggered by Signal Messages just like normal messages. A Signal Message is merely a Message with type “Signal:Z” [E: again I suggest using classes instead of strings] [T: Agreed]
MBSubscription* sub = new MBSubscription();
sub→subscribeType(“Signal:Z”);
int id = module→addSubscription(sub);