- PVSM.RU - https://www.pvsm.ru -
В этой статье хочу познакомить уважаемых читателей с ещё одним велосипедом подходом к организации асинхронного кода. Сразу оговорюсь, что существует масса решений от лёгких потоков и разных предложений по Promise до самопала под специфические задачи, но я не берусь приводить какие-либо субъективные сравнения, поскольку ни одно из них меня не устроило не только с точки зрения программиста, но и проверяющего код.
FutoIn — с одной стороны, это «клей» из стандартов/спецификаций разных мастей для унификации программных интерфейсов различных существующих проектов по устоявшимся типам, с другой — это концепция для построения и масштабирования компонентов проекта и инфраструктуры, написанных на разных технологиях, без потребности в добавления этого самого «клея».
AsyncSteps — это спецификация и реализация программного интерфейса для построения асинхронных программ в независимости от выбранного языка или технологии.
Цели, поставленные для концепции:
Родилась и обновлялась спецификация (назвать стандартом без достаточного распространения и редакторской правки рука не поднимается) FTN12: FutoIn Async API [1]. Сразу скажу, что написана она на английском — де-факто стандарте в международном IT сообществе, как латинский в медицине. Прошу не акцентировать на этом внимание.
Пройдя относительно короткий путь proof-of-concept на базе PHP [2] (ещё не реализованы последние изменения спецификации), родился вариант на JavaScript под Node.js и browser [3]. Всё доступно на GitHub под лицензией Apache-2 [4]. В NPM и Bower доступны под названием «futoin-asyncsteps» [5].
Начнём с разминки для когнитивного понимания сути.
Сначала маленький пример псевдо-кода в синхронном варианте:
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 )
А теперь, то же самое, но написанное асинхронно:
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()
}
)
Ожидаемый результат выполнения:
Level 0 func
Level 1 func
Level 1 onerror: myerror
Level 0 onerror: newerror
Level 0 func2: Prm
Думаю, принцип очевиден, но добавим немного теории: асинхронная задача делится на куски кода (шаги выполнения), которые могут выполниться без ожидания внешнего события за достаточно короткое время чтобы не навредить другим квази-параллельно исполняемым в рамках одного потока. Эти куски кода заключаются в анонимные функции, которые добавляются на последовательное исполнение через метод add() интерфейса AsyncSteps, который реализован на корневом объекте AsyncSteps и доступен через обязательный первый параметр каждой такой функции-шага (именно интерфейс — объекты разные!).
Основные прототипы функций-обработчиков:
Основные методы построения задачи:
Результат выполнения шага:
Результат, переданный через вызов AsyncSteps#success(), попадает в следующий по исполнению шаг в качестве аргументов после обязательного параметра as.
Разберёмся на колбасе кода реальном примере:
// 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();
Результат:
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
Такая несложная конструкция языка как цикл превращается совсем в нетривиальную логику под капотом в асинхронном программировании, в чём можете убедиться лично [6].
Тем не менее, предусмотрены следующие типы циклов:
Досрочное завершение итерации и выход из цикла осуществляется через as.continue( [label] ) и as.break( [label] ) соответственно, которые реализованы на базе as.error( [label] )
Очередной пример, не нуждающийся в особых пояснениях:
// В этот раз в 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();
Результат:
> Repeat: 0 > Repeat: 1 > Repeat: 2 > forEach: 0 = 1 > forEach: 1 = 2 > forEach: 2 = 3 > forEach: a = 1 > forEach: b = 2 > forEach: c = 3
Тут есть два принципиальным момента:
Вызов любого из них потребует вызова явного вызова as.success() или as.error() для продолжения.
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 );
Нужны ли комментарии?
.add(
function( as, arg ){
...
},
function( as, err )
{
console.log( err + ": " + as.state.error_info );
console.log( as.state.last_exception.stack );
}
)
Если всё совсем плохо, то можно «развернуть» код в синхронное выполнение.
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 );
Для тех, кто начинает читать отсюда. Сверху изложено что-то вроде сжатого перевода README.md [7] проекта и выдержек из спецификации FTN12: FutoIn Async API [1]. Если перевариваете английский, то не стесняйтесь получить больше информации из оригиналов.
Идея и проект родились из потребности перенесения бизнес-логики в асинхронную среду. В первую очередь для обработки транзакций базы данных с SAVEPOINT и надёжным своевременным ROLLBACK в среде выполнения вроде Node.js.
FutoIn AsyncSteps — это своего рода швейцарский нож с жёстко структурированными шагами; с развёртыванием стека при обработке исключений практически в классическом виде; с поддержкой циклов, ограничения по времени выполнения, обработчиков отмены задачи в каждом вложенном шаге. Возможно, это именно то, что вы искали для своего проекта.
Был рад с вами поделиться и буду рад получить как положительную, так и негативную критику, котороя пойдёт на пользу проекту. А так же, приглашаю всех интересующих к участию.
P.S. Примеры практического применения FutoIn Invoker [8] и FutoIn Executor [9], о которых, возможно, тоже будет статья после первого релиза.
Автор: andvgal
Источник [10]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/80915
Ссылки в тексте:
[1] FTN12: FutoIn Async API: https://github.com/futoin/specs/blob/master/final/ftn12_async_api.md
[2] на базе PHP: https://github.com/futoin/core-php-ri-asyncsteps
[3] вариант на JavaScript под Node.js и browser: https://github.com/futoin/core-js-ri-asyncsteps
[4] Apache-2: http://www.apache.org/licenses/LICENSE-2.0
[5] «futoin-asyncsteps»: https://www.npmjs.com/package/futoin-asyncsteps
[6] лично: https://github.com/futoin/core-js-ri-asyncsteps/blob/master/lib/asyncstep_protector.js#L226
[7] README.md: https://github.com/futoin/core-js-ri-asyncsteps/blob/master/README.md
[8] FutoIn Invoker: https://github.com/futoin/core-js-ri-invoker
[9] FutoIn Executor: https://github.com/futoin/core-js-ri-executor
[10] Источник: http://habrahabr.ru/post/249087/
Нажмите здесь для печати.