(mOSAIC) API classification
Contents
API categories based on purpose
In-process (internal) API's
Characteristics:
- used directly by the programmer by means of methods, procedures, functions, data structures or other language-specific means;
- in general algorithms, data-structures, GUI's, and interaction with local OS resources (files, etc.) best fit this category;
- usually the developer makes some strong -- and sometimes incorrect -- assumptions about the properties of the API, like:
- no latency as the user expects that when calling the method it shall execute its task in a couple of micro seconds, and return back the answer;
- exceptional cases are of mainly two categories:
as due to bugs in the source code of either the developer or the library (exceptions like NullPointerException or IllegalArgumentsException), in which case the exceptions are just propagated to upper layers just to be logged followed by either shuting down the application or ignoring them;
- as due to normal operating conditions -- the invoice record doesn't exist in the database, the imported file has a wrong format, etc. -- which in general are deterministic -- no matter how many times I try to find the invoice in the database it still doesn't exist unless someone creates it;
- the behavior of the library itself is quite deterministic;
- the procedural units -- methods, functions or procedures depending on the language -- are synchronous: the procedure doesn't return control unless the task was completed (either successfully or not);
- RPC style API's like the ones provided by Java's RMI or other remoting technologies don't quite fall into this category because of the previous assumptions;
Inter-process internal API's
Characteristics:
- still used directly by the programmer exactly as the previous category;
- in this case the API exposes a networked resource or service;
- the API implementation is just a thin layer code that encodes and decodes network messages (see the following category);
- perfect candidates for these kinds are auto-generated proxies by remoting tools like: RMI, Axis, etc.;
- all the previous assumptions are just plain incorrect as:
- latency is often in terms of tens of milliseconds, and in case of web services actually in terms of seconds;
- there is also a third category of exceptional cases: communication problems, that often can't be either handled locally or ignored, and thus the code must take into account re-startable operations;
- the behavior is not deterministic as the same exposed resource is controlled by multiple concurrent entities;
- although the procedural units are most of the times presented as being synchronous, they are actually implemented beneath as busy waiting for a reply;
Inter-process external API's
Characteristics:
- although we call this category API's there are actually protocols;
- the normal user is never exposed to the
- their primary model is an asynchronous one;
mOSAIC API layer to API category mapping
mOSAIC API layer
API category
Cloudlet API
In-process (internal) API
Connector API
Inter-process (internal) API
Interoperability API
Inter-process (internal + external) API
Driver API
Inter-process (internal) API
Native API
Inter-process (internal) API
Native Protocol
Inter-process (external) API
API categories based on programming model
Synchronous (blocking) API's
Characteristics:
- the main characteristic is that once the procedural unit is called, it will return only after the task was either successfully completed, or an error was encountered;
- normally the success is denoted and obtained by a return value, meanwhile the failure by throwing an exception (or returning a special value);
- all the code runs in the same thread, using the same procedural stack;
- 99% of the written code is synchronous in nature;
Example:
// A small example is given in which the developer takes a message from the queue, // message which represents a key in a key-value store that must have it's value updated, // conforming to a complex time-consuming algorithm that takes as input the old value // from the first store and another value from a second store // (The language is a pseudo Java / JavaScript combination) public void execute_task (queue, first_store, second_store) { while (true) { key = queue.dequeue (); old_value = first_store.get (key); extra_data = second_value.get (key); new_value = compute (old_value, extra_data); first_store.put (key, new_value); queue.acknowledge (key); } }
Observations:
we can handle at most one task per thread, even though a good portion of the time we wait for the network to answer our dequeue, get or put requests;
Asynchronous non-blocking API's
Characteristics:
in large part just as the synchronous one, except for that instead the procedural unit blocks until the task is finished, it starts the execution on a different thread (the user doesn't actually see that), and immediately returns control to the calling thread by returning a token (or Future in Java) that shall be used to inspect if the task was done or not;
Example:
public void execute_task (queue, first_stote, second_store) { while (true) { dequeue_future = queue.dequeue (); dequeue_future.wait_completion (); key = dequeue_future.get_message (); // both the get operations happen in parallel old_value_get_future = first_store.get (key); extra_data_get_future = second_store.get (key); // it doesn't matter which one finishes first, // we need both results futures_wait_completion (old_value_get_future, extra_data_get_future); old_value = old_value_get_future.get_value (); extra_data = extra_data_get_future.get_value (); new_value = compute (old_value, extra_data); new_value_put_future = first_store.put (key, new_value); new_value_put_future.wait_completion (); acknowledge_future = queue.acknowledge (key); acknowledge_future.wait_completion (); } }
Observations:
- we write a little more code than in the synchronous (blocking) case, but the structure is quite the same;
the only optimization we've made is by executing the two get operations in parallel, which should reduce the latency by one forth;
we could have executed both the acknowledge and put in parallel but then if the acknowledge succeeds and the put fails, we've just lost one unexecuted job;
Asynchronous callback driven API's
Characteristics:
- the code changes quite a lot as we have to split the code into procedural units based on: code that initiates actions and code that handles the success or failure of those actions;
- the code might run in different threads and callbacks might be run in parallel, thus the user must explicitly synchronize the code;
- we can't make any assumptions about the call stack;
Example (by using anonymous functions (lambda expressions) like JavaScript provides):
public void execute_task (queue, first_store, second_store) { queue.register_consumer ( function (key) { old_value = null; extra_data = null; enqueued_task = false; compute_task = function () { new_value = compute (old_data, extra_data); first_store.put (key, new_value, function () { queue.acknowledge (key, function () {}); }); }; maybe_done_getting = function () { synchronized (this) { if (old_value is not null && extra_data is not null) if (not enqueued_task) { enqueue_long_running_task (compute_task); enqueued_task = true; } } }; first_store.get (key, function (_old_value) { old_value = _old_value; maybe_done_getting (); }); second_store.get (key, function (_extra_data) { extra_data = _extra_data; maybe_done_getting (); }); }); }
Example (by using anonymous class instances like Java provides:
public void execute_task (queue, first_store, second_store) { queue.register_consumer ( new QueueConsumerCallback () { public void consume (key) { old_value = null; extra_data = null; enqueued_task = false; compute_task = new Runnable () { public void run () { new_value = compute (old_data, extra_data); first_store.put (key, new_value, new AbstractKeyValuePutCallback () { public void putSucceeded () { queue.acknowledge (key, new QueueAcknowledgeCallback () public void acknowledgeSucceeded () {}); } }); } }; maybe_done_getting = new Runnable () { public void run () { synchronized (this) { if (old_value is not null && extra_data is not null) if (not enqueued_task) { enqueue_long_running_task (compute_task); enqueued_task = true; } } } }; first_store.get (key, new AbstractKeyValueGetCallback () { public void getSucceeded (_old_value) { old_value = _old_value; maybe_done_getting.run (); } }); second_store.get (key, new AbstractKeyValueGetCallback () { public void getSucceeded (_extra_data) { extra_data = _extra_data; maybe_done_getting.run (); } }); } }); }
Observations:
in either variant the order of the code seems backward because in order to capture a variable in the generated closure, the variable must be already defined before defining the function; (thus because in first_store.get callback we use the maybe_done_getting callback we must define that first, but because inside maybe_done_getting we use compute_task we must also define that even before, etc.;)
also we must mention that in Java the previous code is even harder to write as you can't capture a non-final variable, thus we can't just say old_value = null and then update it from one of the callbacks, therefore the correct java code would have to use some kind of boxing objects like:
... old_value = new BoxedValue<SomeValueType> (); extra_value = new BoxedValue<SomeExtraType> (); ...
... old_value.set (_old_value); ... extra_value.set (_extra_value); ...
... if (old_value.is_set () && extra_data.is_set ()) ...
... new_value.set (compute (old_value.get (), extra_data.get ())); ...
References:
Asynchronous event driven API's
Characteristics:
- even though the previous category is also event-driven in nature, the way in which events to actions are handled is by providing callback functions or callback instances; in this current category we implement an event loop that consumes all kind of events;
- in general event reactors assure the developer that only one event at the time is going to be dispatched to the same event loop;
Example (by using a single event handling callback):
public void execute_task (queue, first_store, second_store) { register_event_handler (this, new global_state (queue, first_store, second_store)); } public void handle_event (state, event_source, event_type, event_data) { switch (event_type) { initialized : { queue.register_consumer (this, state); } queue_consume : { key = event_data; state = new task_state (state, key); state.first_store.get (key, this, state); state.second_store.get (key, this, state); } key_value_get_succeeded : { if (event_source == state.first_store) state.old_data = event_data; else state.extra_data = event_data; if (state.old_data is not null && state.extra_data is not null) enqueue_long_running_task (this, state); } execute_long_running_task : { state.new_value = compute (old_value, extra_value); } finished_long_running_task : { state.first_store.put (state.key, state.new_value, this, state); } key_value_put_succeeded : { state.queue.acknowledge (state.key, this, state); } queue_acknowledge : { // nothing to do anymore } } } public structure global_state { public queue; public first_store; public second_store; }; public structure task_state extends global_state { public key; public old_data; public extra_data; public new_value; };
Example (by having one method for each handled event type):
public void execute_task (queue, first_store, second_store) { register_event_handler (this, new global_state (queue, first_store, second_store)); } public void handle_initialized (state, event_source, event_type, event_data) { queue.register_consumer (this, state); } public void handle_queue_consume (state, event_source, event_type, event_data) { key = event_data; state = new task_state (state, key); state.first_store.get (key, this, state); state.second_store.get (key, this, state); } public void handle_key_value_get_succeeded (state, event_source, event_type, event_data) { if (event_source == state.first_store) state.old_data = event_data; else state.extra_data = event_data; if (state.old_data is not null && state.extra_data is not null) enqueue_long_running_task (this, state); } public void handle_execute_long_running_task (state, event_source, event_type, event_data) { state.new_value = compute (old_value, extra_value); } public void handle_finished_long_running_task (state, event_source, event_type, event_data) { state.first_store.put (state.key, state.new_value, this, state); } public void handle_initialized (state, event_source, event_type, event_data) { state.queue.acknowledge (state.key, this, state); } public void handle_queue_acknowledged (state, event_source, event_type, event_data) { // nothing to do anymore } public structure global_state { // as above }; public structure task_state extends global_state { // as above };
Observations:
each method consume, get, put, etc. has as the last two arguments the event handler and state; (the handler and state could be different, as it happens that when we receive queue_consume event the state is the global one, but we change to another state which is the one also containing the key, and temporary data during the interaction with various resources;
- the code seems less nested than in the callback variant, and the order of the code hints to the actual flow of events;
- in some frameworks (almost all) there is no need for the switch as the user has to implement an interface which has one abstract method for each event type;
References:
Netty (Java);
Apache Mina (Java);
Twisted (Python);
Eventlet (Python);
EventMachine (Ruby);
NodeJS (JavaScript);