FutoIn AsyncSteps: Concept and Implementation of Asynchronous Business Logic

In this article I want to introduce dear readers to another bike approach to organizing asynchronous code. I’ll make a reservation right away that there are a lot of solutions from easy flows and various Promise offers to self-made ones for specific tasks, but I don’t presume to bring any subjective comparisons, since none of them suited me not only from the point of view of the programmer, but also the reviewer the code.

FutoIn - on the one hand, it is a “glue” of standards / specifications of various stripes for standardizing the program interfaces of various existing projects on 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 for adding of this "glue".

Asyncsteps- This is a specification and implementation of a software interface for building asynchronous programs regardless of the selected language or technology.

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;
  • the written program should be easy to read (comparable to the classic version);
  • language exceptions should be supported with the ability to intercept and expand the asynchronous stack to the very beginning;
  • convenience is required for writing asynchronous libraries with a single approach for calling, returning results, and handling errors;
  • provide a simple tool for the natural parallelization of independent branches of the program;
  • provide a simple tool for creating asynchronous loops with classic controls (break, continue) and a label to exit nested loops;
  • Provide a place to store the state of executable business logic
  • the ability to cancel the abstract asynchronous task, correctly completing the execution (freeing up external resources);
  • the ability to easily integrate with other asynchronous programming approaches;
  • the ability to limit the time to complete the task and each sub-task separately;
  • the ability to create a task model for copying (improving the performance of critical parts) or use as an object of the first class to pass logic as a parameter (a la callback);
  • make debugging an asynchronous program as comfortable as possible.


What came of it


The specification was born and updated (to call a standard without sufficient distribution and editorial editing, the hand does not rise) FTN12: FutoIn Async API . I must 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.

Having passed the relatively short proof-of-concept path based on PHP (the latest specification changes have not yet been implemented), a JavaScript version was born for Node.js and browser . Everything is available on GitHub under the Apache-2 license . The NPM and Bower are available under the name "futoin-asyncsteps . "

And how to use these things


Let's start with a warm-up for a cognitive understanding of the essence.

First, a small example of a pseudo-code in synchronous form:
    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 thing, 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 Result:
    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 little theory: an asynchronous task is divided into pieces of code (execution steps) that can be executed without waiting for an external event in a short enough time so as not to harm other quasi-parallel executable within the same thread. These pieces of code are enclosed in anonymous functions, which are added for sequential execution through the add () method of the AsyncSteps interface , which is implemented on the AsyncSteps root object and is accessible through the mandatory first parameter of each such function-step (the interface is different!).

The main prototypes of handler functions:
  • execute_callback (AsyncSteps as [, previous_success_args1, ...]) - prototype of the function step execution
  • error_callback (AsyncSteps as, error) - prototype of the error handling function


The main methods for constructing the task:
  • as.add (execute_callback func [, error_callback onerror]) - adding a step
  • as.parallel ([error_callback onerror]) - returns the AsyncSteps interface of parallel execution


The result of the step:
  • as.success ([result_arg, ...]) is a positive execution result. 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 spins through all onerror (and oncancel, but for now let it go )

The result passed through the call to AsyncSteps # success () falls into the next execution step as arguments after the required as parameter.

Let's look at the sausage code of a real example:

// CommonJS вариант. В browser'е доступно через глобальную переменную $as
var async_steps = require('futoin-asyncsteps');
// Создаём корневой объект-задачу, все функции поддерживают вызов по цепочке
var root_as = async_steps();
// Добавляем простой первый шаг
root_as.add(
    function( as ){
        // Передаём параметр в следующий шаг
        as.success( "MyValue" );
    }
)
// Второй шаг
.add(
    // Шаг программы, аналогичен блоку try
    function( as, arg ){
        if ( arg === 'MyValue' ) // true
        {
            // Добавляем вложенный шаг
            as.add( function( as ){
                // Поднимаем исключение с произвольным кодом MyError и необязательным пояснением
                as.error( 'MyError', 'Something bad has happened' );
            });
        }
    },
    // Второй необязательный параметр - обработчик ошибок, аналогичен блоку catch
    function( as, err )
    {
        if ( err === 'MyError' ) // true
        {
            // продолжаем выполнение задача, игнорируя ошибку
            as.success( 'NotSoBad' );
        }
    }
)
.add(
    function( as, arg )
    {
        if ( arg === 'NotSoBad' )
        {
            // То самое необязательное пояснение доступно через состояние задачи as.state.error_info
            console.log( 'MyError was ignored: ' + as.state.error_info );
        }
        // Добавляем переменные в состояние задачи, доступное на протяжении всего выполнения
        as.state.p1arg = 'abc';
        as.state.p2arg = 'xyz';
        // Следующие два шага, добавленные через p, будут выполнены параллельно.
        // Обратите внимание на результат выполнения, приведённый ниже
        var p = as.parallel();
        p.add( function( as ){
            console.log( 'Parallel Step 1' );
            as.add( function( as ){
                console.log( 'Parallel Step 1.1' );
                as.state.p1 = as.state.p1arg + '1';
                // Подразумеваемый вызов as.success()
            } );
        } )
        .add( function( as ){
            console.log( 'Parallel Step 2' );
            as.add( function( as ){
                console.log( 'Parallel Step 2.1' );
                as.state.p2 = as.state.p2arg + '2';
            } );
        } );
    }
)
.add( function( as ){
    console.log( 'Parallel 1 result: ' + as.state.p1 );
    console.log( 'Parallel 2 result: ' + as.state.p2 );
} );
// Добавляем задачу в очередь на выполнение, иначе "не поедет"
root_as.execute();


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


Get complicated before the cycles


This simple design language as the cycle turns in a completely non-trivial logic under the hood in asynchronous programming, what can make personally .

However, the following cycle types 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 carried out through as.continue ([ label ]) and as.break ([ label ]) respectively, which are implemented on the basis of as.error ([ label ])

Another example that does not need special explanations:
// В этот раз в browser
$as().add(
	function( as ){
		as.repeat( 3, function( as, i ) {
			console.log( "> Repeat: " + i );
		} );
		as.forEach( [ 1, 2, 3 ], function( as, k, v ) {
			console.log( "> forEach: " + k + " = " + v );
		} );
		as.forEach( { a: 1, b: 2, c: 3 }, function( as, k, v ) {
			console.log( "> forEach: " + k + " = " + v );
		} );
	}
)
.loop( function( as ){
	call_some_library( as );
	as.add( func( as, result ){
		if ( !result )
		{
			// exit loop
			as.break();
		}
	} );
} )
.execute();


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:
  1. as.setCancel (func (as)) - the ability to install an external cancellation handler
  2. as.setTimeout (timeout_ms) - setting the maximum timeout

Calling either of them will require calling an explicit call to as.success () or as.error () to continue.

function dummy_service_read( success, error ){
    // Должна вызвать success() при наличии данны
    // или error() при ошибке
}
function dummy_service_cancel( reqhandle ){
    // Чёрная магия по отмене dummy_service_read()
}
var as = async_steps();
as.add( function( as ){
    setImmediate( function(){
        as.success( 'async success()' );
    } );
    as.setTimeout( 10 ); // ms
    // Нет неявного вызова as.success() из-за вызова setTimeout()
} ).add(
    function( as, arg ){
        console.log( arg );
        var reqhandle = dummy_service_read(
            function( data ){
                as.success( data );
            },
            function( err ){
                if ( err !== 'SomeSpecificCancelCode' )
                {
                    try {
                        as.error( err );
                    } catch ( e ) {
                        // Игнорируем исключение - мы не в теле функции-шага
                    }
                }
            }
        );
        as.setCancel(function(as){
            dummy_service_cancel( reqhandle );
        });
        // Нет неявного вызова as.success() из-за вызова setCancel()
        // OPTIONAL. Ожидание не больше 1 секунды
        as.setTimeout( 1000 );
    },
    function( as, err )
    {
        console.log( err + ": " + as.state.error_info );
    }
).execute();
setTimeout( function(){
    // вызывается на корневом объекте
    as.cancel();
}, 100 );


Sugar for debugging


Do you need 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 into 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 a bit of a concise translation of the README.md project and excerpts from the FTN12 specification: FutoIn Async API . If you are digesting English, then feel free to get more information from the originals.

The idea and the 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- This is a kind of Swiss knife with tightly structured steps; with the deployment of the stack when handling exceptions almost in a classic form; with support for loops, runtime restrictions, 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 everyone interested to participate.

PS Examples of practical applications of FutoIn Invoker and FutoIn Executor , which, possibly, will also be an article after the first release.

Also popular now: