Future & Promise

WARNING: This document is about newer implementation that was created by ProFUSION, it's already in GIT, but not exposed in eolian as a special keyword (ie: future<type>) yet, that will happen once the legacy code is removed.

Future & Promise

Introduction

Intro to Asynchronous Programming

Asynchronous programming is needed when you don't know when an event will happen and you can't block the whole program execution to wait for it. Instead you continue your program execution and wait for the event to happen asynchronously -- at some point in time the system will alert you about it, this is usually referred as call back.

The way the system will alert you (call you back) varies amongst implementations. At the very basic level CPUs will do that using interruption services and interruption service handlers (callback specification). Interruptions are preemptive: your application execution will be stopped at an unpredictable state, then you handle it and exit, resuming execution. Since it's unpredictable, it's hard to use and Operating Systems usually abstract that for you.

With UNIX Operating Systems you can get notified in your process using two methods:

Signals: mimic interruptions as they are preemptive, thus unpredictable and painful to use.

Poll/Select: one specifies a series of possible events it's interested in and then "sleeps" waiting for one of those events to happen. It's a voluntary and thus will happen in a predictable fashion, very easy to use.

EFL (Ecore) uses poll/select as core for its main loop, converting signals in a safe way. Programmers will register events (timers, file descriptors, jobs, events and signals) and these will be posted to a queue. Once execution returns to the main loop (ie: all the callback stack for one event), then the next event is processed and if there is a handler (callback specification), then the programmer's code is called back. It's important to note that it's all cooperative, the programmer's code is never interrupted. This means that when you ask for a "timeout in 1.0", it will likely be delayed a little bit, since another event callback may be doing its processing and can't be interrupted! Thus to cooperate well and keep system responsive*, these callback functions must be short-lived**, usually with time boundary that keeps the User Interface (or system in general) responsive -- a "frame time" (usually 1/60 seconds).

Cooperative Tasks

QUESTION: How to keep functions short lived if you may need to do processing that takes multiple frame times?

ANSWER: You segment your work into multiple pieces and go back to the main loop after a reasonable time (ie: usually smaller than a frame time)

EXAMPLE: whenever you have to operate on an unbounded number of items (thus potentially larger than frame time), you may segment the work in units you know will fit in a frame time, registering for a "job" (or "idler", depending on the priority of your work related to others) to handle the rest:

voidmy_event_handler(my_ctx*ctx){inti;for(i=ctx->start;i<N&&(i-ctx->start)<5;i++)do_something(ctx);if(i<N)schedule_new_event(ctx);// still work to do...}

Chaining Problem

This example was very simple on purpose, however in many cases you'd be chaining multiple components, such as wait for an image to be loaded asynchronously (disks may be slow), then execute some heavy operation in a thread, then save it to disk asynchronously. While this is very simple to describe in text, it's not so simple to see in traditional C code as it's not condensed in a single place, rather scattered across multiple callbacks! It would look like:

See that is hard to find what's happening after img_load(), you need to go to its callback to see -- note that the callbacks were declared in a mixed order so you don't guess, it's a mistake to think code will be organized in a multi-person project.

Also see that the error handling is painfully spread, as well as cancellation. It's referred as Callback Hell as it's hard to visualize, it's easy to get wrong due spread error handling and cancellation.

We should be able to describe that more easily and clearly.

Promises and Futures

This is what Promises and Futures are about: to describe a chain of asynchronous events. It can be understood like a pipe, a flow of information. In many places it's only referred as Promises, however in C/EFL we'll handle them as 2 distinct roles:

Promise is a "value pending resolution", that is. It's a promise that you'll either get a value or an error. That's the only possible lifecycle of a promise: either you resolve (fulfill) or you reject it, then it's gone! You can't resolve or reject it more than once, and you must resolve it once to destroy it (even if it's with an error, such as ECANCELED)

Future is a callback that specifies what to do with a resolved value. They're chain-able, also referred as then-able, and they must pass thru a received value or produce new values for the next element in the chain, including produce new promises, that will wait to be resolved before the next future is called.

The value received by a future can be translated to something else, including a different type. This includes errors, which can be handled and converted to non-errors. Examples:

A promise to query the database may return a set of rows (ie: RowSet). A future can convert that into a single row (ie: Row), followed by another future that converts that to a cell (ie: Cell).

A promise to open a file in the disk can return an error (ENOENT). A future may handle that error and instead create a new file, propagating either the file OR another error.

Futures are not mandatory. Whenever a promise is returned and nobody adds a future for it, it will still resolve asynchronously, however nobody will be called back, as there are no callbacks attached.

Promises/A+

Promises gained lots of traction in JavaScript as it's widely used and the "callback hell" problem started to be a problem for not-so-skilled developers (after all, it's very easy to get the "callback hell" wrongly). They even created a very good specification called Promises/A+.

JavaScript particularities aside, what it says is:

a promise can be resolved or rejected only once. It's not possible to reject a previously resolved, neither resolve a previously rejected promise.

futures callbacks must be dispatched in a clean stack, in their terms "execution context stack contains only platform code". In our terms: it's called directly from the main loop.

Since future callbacks will always go to the main loop, you can expect the following code:

EFL Promises and Futures were implemented following Promises/A+ as close as possible, with a key difference:

IMPORTANT: EFL futures can be cancelled with eina_future_cancel(), that will immediately cancel the whole chain of futures pending resolution, independently of which future you cancel -- usually you'll just keep the reference to the last future, cancel it and every Eina_Future will be called back with error ECANCELED, even if the previous future did handle the error and translated it into a non-error! If any promises existed, their cancel callback will be called immediately.

That is, once eina_future_cancel() is called then all Eina_Future_Cb will be called with ECANCELED in the current context/stack. Likewise, if any Eina_Promise were pending resolution, its cancel is called in the current context/stack. It's not going back to the main loop to do so!

This behavior is required since, unlike JavaScript, our core is in C and not reference counted. You may want to cancel the callback and right away free(ctx) they could use. That said, if error is ECANCELED, then you may be called back from an unsafe context.

Another difference is that JavaScript uses 2 callbacks in "then", one for success and another for error. Since our core is in C and one must always check value type prior to its usage otherwise you may get segmentation fault, the core uses a single callback and the user is expected to check if it's an EINA_VALUE_TYPE_ERROR or something he expects. A nice side effect is that our core is very lightweight on memory, shaving some code and pointers. However we offer eina_future_cb_easy() that will handle type checking for you and offer 3 callbacks: success, error and free (always called), this can be used with eina_future_then_from_desc() (eina_future_then() is a macro that calls it) or eina_future_chain().

EFL Implementation

EFL promise and future are very lightweight on dependencies and implemented at Eina layer. However each Eina_Promise that is created should provide an Eina_Future_Scheduler that is used to schedule the future delivery in a safe context.

Whenever using the full EFL stack, this is usually the Ecore main loop and the scheduler should be fetched from Efl.Loop object using efl_loop_future_scheduler_get() (read-only Eo property: future_scheduler). This will cope with multiple main loops, each promise can be bound to a specific main loop.

Our implementation is very lightweight, namely:

Type

32 bits

64 bits

Eina_Promise1

16 bytes

32 bytes

Eina_Future1

28 bytes

56 bytes

Eina_Future_Cb_Easy

48 bytes (28 + 20)

96 bytes

Efl_Future_Cb

48 bytes (28 + 20)

96 bytes

Eina_Value2

12 bytes

16 bytes

1 These are allocated out of Eina_Mempool to avoid pressure on memory allocator and fragmentation.

2Eina_Value is usually passed as value, thus not allocate but uses the stack. For complex types such as strings, stringshare, arrays or structures it may allocate more memory.

Promise

This is the write-side of the pipe, once created it must be either eina_promise_resolve() or eina_promise_reject() to finalize. During its created one must specify a cancel callback, which is responsible to abort the pending process, or at least detach it so when it finishes it won't use any resources that could lead to a crash -- including the cancelled Eina_Promise! If the process cannot be aborted, then at least store the Eina_Promise * somewhere and have cancel to turn it NULL, once the process finishes check that pointer to see if it's still valid.

WARNING: before you write a dummy_cancel think twice! It's usually a mistake that will lead to segmentation faults. There is a reason why the callback is mandatory

Once resolved, the value is owned by the promise delivery system. It's noteina_value_copy()ed, just the pointers will be kept alive until the value is dispatched to some future or there are no more futures. The value can be passed thru futures unchanged, in this case it's kept alive until the first future that returns a new value. Just then the eina_value_flush() will be called.

WARNING: 1. do not call eina_value_flush() on values given to eina_promise_resolve()

WARNING: 2. do not pass Eina_Value that contains on-stack references. Existing types and their set() will always duplicate stuff (ie: strings), however in some cases you may manually craft them, or use EINA_VALUE_TYPE_BLOB or some other types that may allow for such behavior. Always pass values that can survive your context to vanish

Future

This is the read-side of the pipe. A future is either created for a promise using efl_future_new() or for another future, also known as "chain" or "then", using efl_future_then(). A helper is provided to nicely create a chain without too much nesting that would be required in C (eina_future_chain()), instead taking a NULL terminated array of callbacks.

WARNING: 1. do not call eina_value_flush() on values given to your callback. They are const for a reason.

WARNING: 2. do not return Eina_Value that contains on-stack references. Existing types and their set() will always duplicate stuff (ie: strings), however in some cases you may manually craft them, or use EINA_VALUE_TYPE_BLOB or some other types that may allow for such behavior. Always return values that can survive your context to vanish

WARNING: 3. the future chain is not a tree, that means one future can have at most one "then". If you ever "then" an exiting future you must start to use the result for new operations, such as you return the new then in your function.

IMPORTANT: 1. always check the received value.type before eina_value_get(), eina_value_pget() or eina_value_vget(). We're talking about C, then if you expected an EINA_VALUE_TYPE_INT but received EINA_VALUE_TYPE_STRING calling eina_value_get(a_string_value, &intvar) will cause stack corruption on 64 bits machines (where string pointers are 8 bytes, while ints are 4). You can only omit that if using eina_future_cb_easy() and specifying a success_type. There are some helpers such as eina_value_int_get() to check-and-get, please use them.

IMPORTANT: 2. always check for value.type == EINA_VALUE_TYPE_ERROR, they may always happen even if the process shouldn't produce errors, such when a future is cancelled. You can only omit that if using eina_future_cb_easy()

IMPORTANT: 3. always handle errors, at least have the last Eina_Future to handle them. Unhandled errors will produce error logs. One can ignore errors using eina_future_cb_ignore_error() as the last future, it will return empty value for ignored errors and pass thru everything else.

IMPORTANT: 4. you can, and often will, just return the same value you received as parameter. This is called pass thru and is recommended by all functions that have no need to convert/translate the given value. Avoid returning (Eina_Value){ 0 } (EINA_VALUE_EMPTY), prefer to pass thru!

There are set of helpers such as eina_value_int_init() that will setup the value for a given type and set its contents, these are useful to have one-line returns for basic types.

Racing Futures: winner takes all (Race)

A common pattern for futures is to run many in parallel and once the first resolves, cancel the rest. For instance one can create a task and then a timeout, race both and get the task cancelled if takes too long -- if it resolves soon enough, timeout is automatically cancelled.

race

staticEina_Valuefinished(void*dataEINA_UNUSED,constEina_Valuevalue,constEina_Future*dead_futureEINA_UNUSED){if(value.type==EINA_VALUE_TYPE_ERROR){// always handle error (ie: ENOMEM, ECANCELED...)Eina_Errorerr;eina_value_get(&value,&err);fprintf(stderr,"ERROR: finished with #%d %s\n",err,eina_error_msg_get(err));}elseif(value.type==EINA_VALUE_TYPE_STRUCT){unsignedintidx;Eina_Valueresult;// race result is a struct with "index" and "value" membersif(eina_value_struct_get(&value,"index",&idx)&&eina_value_struct_get(&value,"value",&result)){char*str=eina_value_to_string(&result);printf("Future %s won! Result: %s\n",idx==0?"download":"timeout",str);free(str);}else{fprintf(stderr,"ERROR: failed to fetch race result members!\n");returneina_value_error_init(EINVAL);}}returnvalue;// pass thru the results}voidinit(Efl_Object*loop){Eina_Future*download=do_download(loop,"http://www.enlightenment.org");Eina_Future*timeout=efl_loop_timeout(loop,10.0);eina_future_then(eina_future_race(download,timeout_future),.cb=finished);// finished is called once download or timeout resolve (the first one)}

Waiting many futures (All)

Another common pattern for futures is to run many in parallel and wait all of them to resolve, then provide an array of resolved values. Note that the values are in the same order as the promises.

Helper Future Callbacks

Eina_Future_Desc allows for helper callbacks to be generated and passed easily to eina_future_then_from_desc(), eina_future_chain() or eina_future_chain_array(). These may allocate data and return in Eina_Future_Desc::data without worries as the protocol will always call Eina_Future_Desc::cb, even on errors or when the future is cancelled -- thus no leaks should occur.

Type Convert

eina_future_cb_convert_to(type) returns an Eina_Future_Desc that will convert the future result to the given type.

Print to console

eina_future_cb_console_from_desc() or its syntax sugar eina_future_cb_console() returns an Eina_Future_Desc that will print out the results to stdout with an optional prefix and suffix. Received value will be passed thru unchanged.

print_to_console

eina_future_chain(something_that_produces_a_future(),eina_future_cb_console(),// default prefix ("") and suffix ("\n")eina_future_cb_console(.prefix="something produced: "),// named parametereina_future_cb_console("something produced: "),// positional parametereina_future_cb_console(.suffix=", is it right?\n"),// named parameter, include "\n" if it's needed!eina_future_cb_console(NULL,", is it right?\n"),// positional parameter, include "\n" if it's needed!);

Easy Callbacks

Eina_Future_Desc is lean and keeps core very efficient and simple. However sometimes users want to avoid cumbersome tasks such as check for error and differentiate it from regular value, validate expected success type and so on. Then eina_future_cb_easy_from_desc() uses Eina_Future_Cb_Easy_Desc to provide more details, resulting in a wrapper callback that is every easy to use. Syntax sugar exists as eina_future_cb_easy() to be used with eina_future_then_from_desc() or eina_future_chain(); eina_future_then_easy() and eina_future_chain_easy() will make it even simpler if you want an even simpler path.

easy_chain

eina_future_chain_easy(something_that_produces_a_future(),{.success=just_success_cb,.data=some_data},{.error=just_error_cb},{.free=just_monitor_and_free,.data=other_data},{.success=success_if_type_is_int,.success_type=EINA_VALUE_TYPE_INT},{.success_type=EINA_VALUE_TYPE_INT});// only success_type will enforce type or convert to EINVAL error

Efl_Object (Eo) integration

Binding Future to an Object

Usually a promise or future must be bound to an object life, be the owner object responsible to resolve it or some user that want to bind (link) to its own lifecycle.

This is done with the Eina_Future_Desc returned by efl_future_cb_from_desc() or its syntax sugar macro efl_future_cb() which mimics eina_future_cb_easy() behavior, however instead of a general void *data it uses an Efl_Object *o and binding to its lifecycle. As usual, this Eina_Future_Desc can be used with eina_future_then_from_desc() or eina_future_chain(). If more than one is to be used, then the helper efl_future_chain_from_array() or efl_future_chain() (syntax sugar) can be used as listed below.

When efl_future_cb_from_desc() or variants are used, the given Eina_Future will be chained to a new one that will:

monitor object death, once the object destructor runs all pending futures will be cancelled with eina_future_cancel();

Use case: an object creates a promise and returns a future that is bound to the object lifecycle.

NOTE: this should be done automatically by Eolian when return: future<>.

return_bound_future

EOLIANstaticEina_Future*_my_class_method_creates_a_future(Efl_Object*o,My_Class_Data*pd){pd->promise=eina_promise_new(efl_loop_future_scheduler_get(efl_loop_get(o)),_promise_cancel,o);returneina_future_then_from_desc(eina_future_new(pd->promise),efl_future_cb(o));// when used like this, simply binds lifecycle}

Use case: an object creates a promise and returns a future that is bound to the object lifecycle AND checks for resolved type.

NOTE: this should be done automatically by Eolian when return: future<TYPE>.

Check and return Efl_Object as Eina_Value

Promises resolve passing Eina_Value, these are then forwarded to futures, which can query its contents, pass thru the received value or create a new one to return.

Then EINA_VALUE_TYPE_OBJECT is provided to manage Efl_Object, it will increase reference on eina_value_set() and decrease reference when a value is replaced with another eina_value_set() or when the value is flushed with eina_value_flush(). As done with EINA_VALUE_TYPE_STRING, EINA_VALUE_TYPE_STRINGSHARE and others, eina_value_get() will NOT increment reference, do that yourself.

Hints for Bindings

Bindings should wrap Eina_Future and Eina_Promise in their native solutions, if any. For example in JavaScript it should interoperate and behave such as JS Promise.

You must always provide a cancel callback to your promise and cleanup the binding wrapper object.

In your future callback, convert Eina_Value_Type to a language type, convert EINA_VALUE_TYPE_OBJECT to Eo binding wrappers as well as EINA_VALUE_TYPE_ERROR to language error types/exceptions, such as TypeError in JavaScript or Exception in Python.

In your future callback, catch language exceptions and convert them to EINA_VALUE_TYPE_ERROR. An user must be able to throw new Error("message") and that will result in EINA_VALUE_TYPE_ERROR being returned. If possible, convert to existing Eina_Error, such as errno.h (in Python OSError can allow you to easily do it), otherwise register your own Eina_Error such as eina_error_msg_register("generic Python exception") or keep a hash language error type -> Eina_Error, if it doesn't exist you register one using its name (recommended).

Manually expose something like efl_future_from_desc(). Manual bindings should be done to keep it "native" to the target language, then you call efl_future_from_desc() with your own wrapper.

Integrate a lambda pre-processor, allowing functions to be provided inline and the preprocessor will handle emitting that as its own function and passing that to the future description. (Raster mentioned some existing tool)