In this article I want to acquaint dear readers with another
bike approach to the organization of asynchronous code. At once I will make a reservation that there is a mass of solutions from light streams and various proposals on Promise to self-promotion for specific tasks, but I do not undertake to give any subjective comparisons, since none of them did not suit me not only from the point of view of the programmer, but also the verifier code.
FutoIn - on the one hand, is the “glue” of standards / specifications of various suits for unifying software interfaces of various existing projects by well-established types, on the other hand, it is a concept for building and scaling project components and infrastructure written on different technologies without the need to add this very "glue".
AsyncSteps is the specification and implementation of a software interface for building asynchronous programs regardless of the language or technology selected.
')
Goals set for the concept:- implementation (with reservations) should be possible in all common programming languages with support for objects and anonymous functions. Representative minimum: C ++, C #, Java, JavaScript, Lua (not OOP), PHP, Python;
- written program should be easy to read (comparable to the classic version);
- Exceptions must be supported with the ability to intercept and expand the asynchronous stack to the beginning;
- Convenience is required for writing asynchronous libraries with a single approach for calling, returning a result and handling errors;
- provide a simple tool for the natural parallelization of independent program branches;
- provide a simple tool for creating asynchronous loops with classical controls (break, continue) and a label to exit nested loops;
- provide a place to store the state of the executable business logic;
- the ability to cancel the abstract asynchronous task, correctly completing the execution (freeing external resources);
- ability to easily integrate with other approaches to asynchronous programming;
- the ability to limit the execution time of the task and each subtask separately;
- the ability to create a task model for copying (improving the performance of critical parts) or using it as a first-class object to transfer logic as a parameter (a-la callback);
- make debugging asynchronous program as comfortable as possible.
What came of it
The specification was born and updated (to call the standard without a sufficient distribution and editing the hand does not rise)
FTN12: FutoIn Async API .
I’ll say right away that it is written in English - the de facto standard in the international IT community, like Latin in medicine. Please do not focus on this attention.Having passed a relatively short path
based on PHP-based proof-of-concept (the latest specification changes have not yet been implemented),
a JavaScript version has been born
under Node.js and browser . Everything is available on GitHub under the
Apache-2 license. In NPM and Bower are available under the name
"futoin-asyncsteps" .
And how is this used?
Let's start with a warm-up for a cognitive understanding of the essence.
First, a small example of a
pseudo-code in the synchronous version:
variable = null
try
{
print ("Level 0 func")
try
{
print ("Level 1 func")
throw "myerror"
}
catch (error)
{
print ("Level 1 onerror:" + error)
throw "newerror"
}
}
catch (error)
{
print ("Level 0 onerror:" + error)
variable = "Prm"
}
print ("Level 0 func2:" + variable)
And now, the same, but written asynchronously:
add (// Level 0
func (as) {
print ("Level 0 func")
add (// Level 1
func (as) {
print ("Level 1 func")
as.error ("myerror")
},
onerror (as, error) {
print ("Level 1 onerror:" + error)
as.error ("newerror")
}
)
},
onerror (as, error) {
print ("Level 0 onerror:" + error)
as.success ("Prm")
}
)
add (// Level 0
func (as, param) {
print ("Level 0 func2:" + param)
as.success ()
}
)
Expected output:
Level 0 func
Level 1 func
Level 1 onerror: myerror
Level 0 onerror: newerror
Level 0 func2: Prm
I think the principle is obvious, but let's add a bit of theory: the asynchronous task is divided into pieces of code (execution steps) that can be executed without waiting for an external event for a sufficiently short time so as not to harm others quasi-parallelly executed within the same thread. These pieces of code are enclosed in anonymous functions that are added for sequential execution via the
add () method of the
AsyncSteps interface, which is implemented on the
AsyncSteps root object and is available through the first parameter of each such step function (the interface is the objects are different!).
Main prototypes of handler functions:
- execute_callback (AsyncSteps as [, previous_success_args1, ...]) is a prototype of the function performing a step
- error_callback (AsyncSteps as, error) - prototype error handling function
The main methods of constructing the task:
- as.add (execute_callback func [, error_callback onerror]) - add step
- as.parallel ([error_callback onerror]) - returns the AsyncSteps interface of parallel execution
The result of the step:
- as.success ([result_arg, ...]) - positive result of execution. Arguments are passed to the next step. Default action - no need to call if no arguments
- as.error (name [, error_info]) - set as.state (). error_info and throw an exception . The asynchronous stack is spun through all onerror (and oncancel, but for now we’ll omit it)
The result passed through a call to
AsyncSteps # success () gets into the next step to be executed as arguments after the required parameter as.
Let's look at the
sausage code real example:
Result:
MyError was ignored: Something bad has happened
Parallel Step 1
Parallel Step 2
Parallel Step 1.1
Parallel Step 2.1
Parallel 1 result: abc1
Parallel 2 result: xyz2
Complicate to cycles
Such a simple language design as a cycle turns into a completely non-trivial logic under the hood in asynchronous programming, which you can see for yourself.
However, the following types of cycles are provided:
- loop (func (as) [, label]) - before the error or as.break ()
- repeat (count, func (as, i) [, label]) - no more than count iterations
- forEach (map_or_array, func (as, key, value) [, label]) - pass through a simple or associative array (or equivalent)
Early termination of the iteration and exit from the loop is done via
as.continue ([ label ]) and
as.break ([ label ]), respectively, which are implemented on the basis of
as.error ([ label ])Another example that does not require special explanations:
Result:
> Repeat: 0
> Repeat: 1
> Repeat: 2
> forEach: 0 = 1
> forEach: 1 = 2
> forEach: 2 = 3
> forEach: a = 1
> forEach: b = 2
> forEach: c = 3
Waiting for an external event
There are two fundamental points here:
- as.setCancel (func (as)) - the ability to install an external cancellation task handler
- as.setTimeout (timeout_ms) - setting the maximum wait time
Calling any of them will require calling an explicit call to as.success () or as.error () to continue. function dummy_service_read( success, error ){
Sugar for debugging
Need any comments?
.add( function( as, arg ){ ... }, function( as, err ) { console.log( err + ": " + as.state.error_info ); console.log( as.state.last_exception.stack ); } )
If everything is completely bad, then you can “deploy” the code to synchronous execution.
async_steps.installAsyncToolTest(); var as = async_steps(); as.state.second_called = false; as.add( function( as ){ as.success(); }, function( as, error ){ error.should.equal( "Does not work" ); } ).add( function( as ){ as.state.second_called = true; as.success(); } ); as.execute(); as.state.second_called.should.be.false; async_steps.AsyncTool.getEvents().length.should.be.above( 0 ); async_steps.AsyncTool.nextEvent(); as.state.second_called.should.be.true; async_steps.AsyncTool.getEvents().length.should.equal( 0 );
Conclusion
For those who start reading from here. Above is described something like a compressed translation of the
README.md project and excerpts from
the FTN12 specification: FutoIn Async API . If you digest English, do not hesitate to get more information from the originals.
The idea and project were born from the need to transfer business logic to an asynchronous environment. Primarily for processing database transactions with SAVEPOINT and reliable timely ROLLBACK in a runtime environment like Node.js.
FutoIn AsyncSteps is a kind of Swiss knife with rigidly structured steps; with the deployment of the stack in the processing of exceptions almost in the classical form; with support for loops, time limits, task cancellation handlers in each nested step. Perhaps this is exactly what you were looking for for your project.
I was glad to share with you and I will be glad to receive both positive and negative criticism, which will benefit the project. And also, I invite all interested to participate.
PS Examples of practical application of
FutoIn Invoker and
FutoIn Executor , about which, perhaps, there will also be an article after the first release.