📜 ⬆️ ⬇️

FutoIn AsyncSteps: the concept and implementation of asynchronous business logic

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:


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:


The main methods of constructing the task:


The result of the step:

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:

// 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


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:


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:
 //     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 here:
  1. as.setCancel (func (as)) - the ability to install an external cancellation task handler
  2. 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 ){ //   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


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.

Source: https://habr.com/ru/post/249087/


All Articles