В этой статье хочу познакомить уважаемых читателей с ещё одним велосипедом подходом к организации асинхронного кода. Сразу оговорюсь, что существует масса решений от лёгких потоков и разных предложений по Promise до самопала под специфические задачи, но я не берусь приводить какие-либо субъективные сравнения, поскольку ни одно из них меня не устроило не только с точки зрения программиста, но и проверяющего код.
FutoIn — с одной стороны, это «клей» из стандартов/спецификаций разных мастей для унификации программных интерфейсов различных существующих проектов по устоявшимся типам, с другой — это концепция для построения и масштабирования компонентов проекта и инфраструктуры, написанных на разных технологиях, без потребности в добавления этого самого «клея».
AsyncSteps — это спецификация и реализация программного интерфейса для построения асинхронных программ в независимости от выбранного языка или технологии.
Цели, поставленные для концепции:
Родилась и обновлялась спецификация (назвать стандартом без достаточного распространения и редакторской правки рука не поднимается) FTN12: FutoIn Async API. Сразу скажу, что написана она на английском — де-факто стандарте в международном IT сообществе, как латинский в медицине. Прошу не акцентировать на этом внимание.
Пройдя относительно короткий путь proof-of-concept на базе PHP (ещё не реализованы последние изменения спецификации), родился вариант на JavaScript под Node.js и browser. Всё доступно на GitHub под лицензией Apache-2. В NPM и Bower доступны под названием «futoin-asyncsteps».
Начнём с разминки для когнитивного понимания сути.
Сначала маленький пример псевдо-кода в синхронном варианте:
А теперь, то же самое, но написанное асинхронно:
Ожидаемый результат выполнения:
Думаю, принцип очевиден, но добавим немного теории: асинхронная задача делится на куски кода (шаги выполнения), которые могут выполниться без ожидания внешнего события за достаточно короткое время чтобы не навредить другим квази-параллельно исполняемым в рамках одного потока. Эти куски кода заключаются в анонимные функции, которые добавляются на последовательное исполнение через метод add() интерфейса AsyncSteps, который реализован на корневом объекте AsyncSteps и доступен через обязательный первый параметр каждой такой функции-шага (именно интерфейс — объекты разные!).
Основные прототипы функций-обработчиков:
Основные методы построения задачи:
Результат выполнения шага:
Результат, переданный через вызов AsyncSteps#success(), попадает в следующий по исполнению шаг в качестве аргументов после обязательного параметра as.
Разберёмся наколбасе кода реальном примере:
Результат:
Такая несложная конструкция языка как цикл превращается совсем в нетривиальную логику под капотом в асинхронном программировании, в чём можете убедиться лично.
Тем не менее, предусмотрены следующие типы циклов:
Досрочное завершение итерации и выход из цикла осуществляется через as.continue( [label] ) и as.break( [label] ) соответственно, которые реализованы на базе as.error( [label] )
Очередной пример, не нуждающийся в особых пояснениях:
Результат:
Тут есть два принципиальным момента:
Вызов любого из них потребует вызова явного вызова as.success() или as.error() для продолжения.
Нужны ли комментарии?
Если всё совсем плохо, то можно «развернуть» код в синхронное выполнение.
Для тех, кто начинает читать отсюда. Сверху изложено что-то вроде сжатого перевода README.md проекта и выдержек из спецификации FTN12: FutoIn Async API. Если перевариваете английский, то не стесняйтесь получить больше информации из оригиналов.
Идея и проект родились из потребности перенесения бизнес-логики в асинхронную среду. В первую очередь для обработки транзакций базы данных с SAVEPOINT и надёжным своевременным ROLLBACK в среде выполнения вроде Node.js.
FutoIn AsyncSteps — это своего рода швейцарский нож с жёстко структурированными шагами; с развёртыванием стека при обработке исключений практически в классическом виде; с поддержкой циклов, ограничения по времени выполнения, обработчиков отмены задачи в каждом вложенном шаге. Возможно, это именно то, что вы искали для своего проекта.
Был рад с вами поделиться и буду рад получить как положительную, так и негативную критику, котороя пойдёт на пользу проекту. А так же, приглашаю всех интересующих к участию.
P.S. Примеры практического применения FutoIn Invoker и FutoIn Executor, о которых, возможно, тоже будет статья после первого релиза.
FutoIn — с одной стороны, это «клей» из стандартов/спецификаций разных мастей для унификации программных интерфейсов различных существующих проектов по устоявшимся типам, с другой — это концепция для построения и масштабирования компонентов проекта и инфраструктуры, написанных на разных технологиях, без потребности в добавления этого самого «клея».
AsyncSteps — это спецификация и реализация программного интерфейса для построения асинхронных программ в независимости от выбранного языка или технологии.
Цели, поставленные для концепции:
- реализация (с оговорками) должна быть возможна на всех распространённых языках программирования с поддержкой объектов и анонимных функций. Репрезентативный минимум: С++, C#, Java, JavaScript, Lua (не ООП), PHP, Python;
- написанная программа должна легко читаться (сравнимо с классическим вариантом);
- должны поддерживаться исключения языка (Exceptions) с возможностью перехвата и разворачиванием асинхронного стека до самого начала;
- требуется удобство для написания асинхронных библиотек с единым подходом для вызова, возврата результата и обработки ошибок;
- предоставить простой инструмент для естественного распараллеливания независимых веток программы;
- предоставить простой инструмент создания асинхронных циклов с классическим управлением (break, continue) и меткой для выхода из вложенных циклов;
- предоставить место для хранения состояния исполняемой бизнес-логики;
- возможность отменять абстрактную асинхронную задачу, правильно завершая выполнение (освобождая внешние ресурсы);
- возможность легко интегрироваться с другими подходами асинхронного программирования;
- возможность ограничивать время выполнения задачи и отдельно каждой подзадачи;
- возможность создавать модель задачи для копирования (улучшения производительности критичных частей) или использования как объект первого класса для передачи логики в качестве параметра (а-ля callback);
- сделать отладку асинхронной программы максимально комфортной.
Что же из этого вышло
Родилась и обновлялась спецификация (назвать стандартом без достаточного распространения и редакторской правки рука не поднимается) FTN12: FutoIn Async API. Сразу скажу, что написана она на английском — де-факто стандарте в международном IT сообществе, как латинский в медицине. Прошу не акцентировать на этом внимание.
Пройдя относительно короткий путь proof-of-concept на базе PHP (ещё не реализованы последние изменения спецификации), родился вариант на JavaScript под Node.js и browser. Всё доступно на GitHub под лицензией Apache-2. В NPM и Bower доступны под названием «futoin-asyncsteps».
И каким же образом сие использовать
Начнём с разминки для когнитивного понимания сути.
Сначала маленький пример псевдо-кода в синхронном варианте:
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 и доступен через обязательный первый параметр каждой такой функции-шага (именно интерфейс — объекты разные!).
Основные прототипы функций-обработчиков:
- execute_callback( AsyncSteps as[, previous_success_args1, ...] ) — прототип функции выполнение шага
- error_callback( AsyncSteps as, error ) — прототип функции обработки ошибок
Основные методы построения задачи:
- as.add( execute_callback func[, error_callback onerror] ) — добавление шага
- as.parallel( [error_callback onerror] ) — возвращает интерфейс AsyncSteps параллельного исполнения
Результат выполнения шага:
- as.success( [result_arg, ...] ) — положительный результат выполнения. Аргументы передаются в следующий шаг. Действие по умолчанию — вызывать не требуется, если нет аргументов
- as.error( name [, error_info] ) — установить as.state().error_info и бросить исключение. Асинхронный стек раскручивается через все onerror (и oncancel, но это пока опустим)
Результат, переданный через вызов 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
Усложняемся до циклов
Такая несложная конструкция языка как цикл превращается совсем в нетривиальную логику под капотом в асинхронном программировании, в чём можете убедиться лично.
Тем не менее, предусмотрены следующие типы циклов:
- loop( func( as ) [, label] ) — до ошибки или as.break()
- repeat( count, func( as, i ) [, label] ) — не более count итераций
- forEach( map_or_array, func( as, key, value ) [, label] ) — проход по простому или ассоциативному массиву (или эквиваленту)
Досрочное завершение итерации и выход из цикла осуществляется через 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.setCancel( func( as ) ) — возможность установки обработчика внешней отмены задачи
- as.setTimeout( timeout_ms ) — установка максимального времени ожидания
Вызов любого из них потребует вызова явного вызова 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 проекта и выдержек из спецификации FTN12: FutoIn Async API. Если перевариваете английский, то не стесняйтесь получить больше информации из оригиналов.
Идея и проект родились из потребности перенесения бизнес-логики в асинхронную среду. В первую очередь для обработки транзакций базы данных с SAVEPOINT и надёжным своевременным ROLLBACK в среде выполнения вроде Node.js.
FutoIn AsyncSteps — это своего рода швейцарский нож с жёстко структурированными шагами; с развёртыванием стека при обработке исключений практически в классическом виде; с поддержкой циклов, ограничения по времени выполнения, обработчиков отмены задачи в каждом вложенном шаге. Возможно, это именно то, что вы искали для своего проекта.
Был рад с вами поделиться и буду рад получить как положительную, так и негативную критику, котороя пойдёт на пользу проекту. А так же, приглашаю всех интересующих к участию.
P.S. Примеры практического применения FutoIn Invoker и FutoIn Executor, о которых, возможно, тоже будет статья после первого релиза.
