×

Пауза, сериализация состояния программы и продолжение

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті

Пытаюсь решить такую проблему сериализации/десериализации программы/процесса.

Допустим, у нас есть программа, которая выполняется (псевдокод):

void main() {
    action_1();
    checkpoint();
    int count = action_2("hello world");
    checkpoint();

    for (i = 0; i < count; i++) {
        action_3(i);
        checkpoint();
    }
}

checkpoint обозначает место, где мы бы хотели сделать «чекпоинт» — конкретно, сохранить текущее состояние программы и прервать ее выполнение. При этом процес ОС может быть полностью уничтожен.

Далее это состояние может быть загружено обратно в память и выполнение программы может быть возобновлено с места последнего чекпоинта. Это может быть сделано даже на другом физическом компьютере, сразу же, или через несколько дней. С точки зрения программы будет выглядеть так будто она никогда не останавливалась, и она продолжит работу как ни в чем ни бывало.

Есть ли технологии, которые позволили бы это запилить? Какие есть варианты?

Пока думаю о таких constraints, в неопределенном порядке:
1) Программу можно написать на одном из мейнстримовых языков программирования.
2) Минимальные требования к окружению (ОС или оборудованию)
3) Сериализированное состояние должно быть как можно меньше
4) Реализация должна быть безопасной (whatever this means)
5) Минимальные ограничения на то, что может использовать программист в своей программе (но разумные обоснованные ограничения допустимы).

Думал о таких вариантах:
1) Сериализировать виртуальную машину языка
Есть языки программирования, которые насколько тривиальны, что состояние выполнения программы легко сериализировать наивным способом (например, brainfuck).

Недостатки: такой язык будет мало полезен на практике.

2) Сохранить все состояние ОС
Использовать какую-то технологию виртуализации, которая позволяет перевести виртуальную машину в режим гибернации, и сохранить весь дамп виртуальной машины.

Недостатки: состояние машины будет достаточно большим (сотни мегабайт)

3) Сохранить дам памяти процесса
Гипотетически, если снять дам памяти процесса, сохранить его на диск, потом снова загрузить в память, теоретически можно возобновить выполнение программы.

Недостатки: после такого «перезапуска» часть компонентов программы может быть неработоспособной. Например, если были отрыты сокеты или файловые дескрипторы, они будут недействительный (или будут ссылаться на какие-то существующие дескрипторы, которые уже невалидны).
Нужно убедиться, что адресация памяти «до» и «после» совпадают, что сложнее сделать в некоторых ОС, которые рандомизируют расположение памяти.
Состояние памяти все еще относительно большое (десятки МБ)

4) Представить граф выполнения программы как «данные» и сериализировать их.
Есть языки программирования, которые позволяют сконвертировать код программы в state machine (например, C# или Rust с его yield/async/await).

Недостатки: проблема расположения данных в памяти становится еще более актуальной, так как мы уже не имеем контроля над ней. Куча данных в состоянии программы не могут быть сериализированы (например, ссылка на другой метод aka делегат).

====

Какие еще есть варианты/идеи/предложения? Мой предварительный поиск информации привел к выводу, что простого решения проблемы нет и нужно будет проделать существенную часть работы, чтоб получить хоть что-то работающее и не имеющее серьезных ограничений. Какое направление, вы считаете, выглядит более обещающим?

👍ПодобаєтьсяСподобалось0
До обраногоВ обраному2
LinkedIn

Найкращі коментарі пропустити

Наш опыт показывает, что лучше дизайнить приложения сразу с возможностью возобновить работу с произвольного места. Типичный пример — fastboot в машинах, во многих luxury cars реализована подобная возможность — выходишь из машины, вытащил ключ зажигания или нажал кнопку start/stop — у всего софта есть пол секунды до того как водитель хлопнул дверью, чтобы сбросить своё состояние на флеш. Это абсолютно не сериализация чего-то, обычно это очень и очень простое состояние — сложность тут ни к чему, если это карта GPS, то сохраняем координаты и вектор, если это музыкальный плеер, то имя файла и позиция и всё в таком же духе, keep it simple. Никто не парится над памятью, таймерами, хендлами и прочим. Когда водитель открывает дверь операционная система с софтом загружается максимум за 2 секунды со всем софтом, в luxury cars от 0.5с до 1с. Весь софт инициализируется заново, как положено, открывает всё, что ему надо и потом просто вычитывает своё последнее состояние и переходит сразу к нему. Просто как двери.

Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Доброго дня панство!
Як зв’язатися з автором посту?
Потрібна аудієнція з данним паном. Допоможете?
Буду дуже вдячний.

Зареєструйтесь та напишіть йому в приват

А как связаться с автором этого комментария?

Можете написать мне в linkedin — www.linkedin.com/in/kryvokrysenko

Цікаво, автор вибрав якийсь варіант? А взагалі добре б знати завдання, для чого це потрібно, якщо це звичайно не спроба створити бібліотеку для подібних речей

Пока хочу исследовать вариант с корутинами.

Провел некоторые базовые эксперименты с корутинами в C# и Rust. Получил обнадеживающие (в C# - можно с помощью рефлексии разобрать состояние корутины, и «перезапустить» ее), и разочаровывающие (в Rust — состояние корутины можно только прочитать как байтовый массив, и непонятно что там внутри и как его сериализировать) результаты.

Думаю запилить рабочий POC на C# и продемонстрировать.

С Rust — нужно будет поковырять компилятор, чтоб понять, можно ли относительно легко извлечь описание структуры корутины и манипулировать им.

А взагалі добре б знати завдання, для чого це потрібно, якщо це звичайно не спроба створити бібліотеку для подібних речей

Выше/ниже писал. На прошлой работе постоянно приходилось пилить какие-то асинхронные workflow на базе SWF и Step Functions. Пришел к тому же выводу, что и Эскобар, и хочется запилить что-то более юзабельное

Если коротко то без редизайн не обойтись. То что вы хотите называется event sourcing. Вот немного общей теории martinfowler.com/...​eaaDev/EventSourcing.html
Готовые фреймворки есть на всех популярных платформах.
Основная идея в том что программа представляет из себя конечный автомат и состояние хранится в виде последовательности событий вызывающих изменение системы, как правило их сравнительно не много по сравнению с чтением этого состояния.

Мне хочется запилить удобный инструмент для моделирования этого конечного автомата без создания новых фреймворков и языков программирования.

Наш опыт показывает, что лучше дизайнить приложения сразу с возможностью возобновить работу с произвольного места. Типичный пример — fastboot в машинах, во многих luxury cars реализована подобная возможность — выходишь из машины, вытащил ключ зажигания или нажал кнопку start/stop — у всего софта есть пол секунды до того как водитель хлопнул дверью, чтобы сбросить своё состояние на флеш. Это абсолютно не сериализация чего-то, обычно это очень и очень простое состояние — сложность тут ни к чему, если это карта GPS, то сохраняем координаты и вектор, если это музыкальный плеер, то имя файла и позиция и всё в таком же духе, keep it simple. Никто не парится над памятью, таймерами, хендлами и прочим. Когда водитель открывает дверь операционная система с софтом загружается максимум за 2 секунды со всем софтом, в luxury cars от 0.5с до 1с. Весь софт инициализируется заново, как положено, открывает всё, что ему надо и потом просто вычитывает своё последнее состояние и переходит сразу к нему. Просто как двери.

Посмотрите в сторону отмотки и сохранения стека:
www.nongnu.org/libunwind

Коментар порушує правила спільноти і видалений модераторами.

Жаль, интересная была тема

1) Мне интересна так как пересекается с опытом
2) Перечисленные подходы можно запомнить и потом применить на практике если вдруг будет похожая задача
3) Общее решение — виртуалка

Так здесь тоже паттерны. Только не проектирования, а решения задачи. Именно поэтому Таненбаума про ОС интересно читать — те же алгоритмы можно применять для других задач.
(говорят, любая программа со временем превращается в ОС)

Ну вот динамическое программирование — это метаалгоритм, то есть — паттерн для создания алгоритмов.
ООП, реактивный подход, макросы — все туда же, в паттерны, только не написания кода, а решения задач.

Паттерн — это общий подход к решению задач определенного типа.
Когда в работе нужно будет сохранять состояние программы — можно вспомнить, что здесь пообсуждали.

Когда в работе нужно будет сохранять состояние программы — можно вспомнить, что здесь пообсуждали.

Взагалі, видати програмісту інтерфейс для збереження і сказати реалізувати — і по ваших 5 пунктах буде ідеально. От тільки писати таке як мінімум займе час, плюс можливі баги. І, мабуть, альтернатива — це таки VM. Тільки все одно якийсь конект по мережі здохне за пару днів та час посунеться. Але для програмера мабуть мінімум напрягу буде.

void main() {
    action_1();
    if (fork() == 0) checkpoint();
    else return 0;
    int count = action_2("hello world");
    if (fork() == 0) checkpoint();
    else checkpoint();

    for (i = 0; i < count; i++) {
        action_3(i);
        if (fork() == 0) checkpoint();
        else return 0;
    }
}

Не понятно, что этот код делает. Можете пояснить?

Этот код про то, как резвящееся на оружейном складе стадо обезьян будет юзать вашу фичу.

Встречный вопрос: если произойдёт вызов checkpoint на 6-ой строке, но не успеет произойти на 7-ой, то как программа будет восстанавливать своё состояние?

Это то как резвящееся на оружейном складе стадо обезьян будет юзать вашу фичу.

Это одна из проблем — как предоставить разрабочтику разумные ожидания о том, что произойдет с программой после чекпоинта, что будет восстановлено, а что нет, что будет в неоднозначном состоянии. При этом не добавляя кучу ограничений, которые бы сделали этот инструмент просто неюзабельным.

Встречный вопрос: если произойдёт вызов checkpoint на 6-ой строке, но не успеет произойти на 7-ой, то как программа будет восстанавливать своё состояние?

Программа восстанавливает состояние с последнего успешного чекпоинта. Каждый чекпоинт как транзакция, он или прошел, или нет. Частично завершенные чекпоинты считаются не завершенными.

Но сколько ж успешных чекпоинтов будет у программы после вызова checkpoint на шестой строке?

Зачем вообще об этом думать, если там явно чушь вместо нормального алгоритма?

Плодит дочерние процессы в том состоянии, в котором был родительский во время вызова fork()

В Azure нечто подобное реализовано в Durable Functions. Но это облако. Локально можно реализовать на основе standalone durable-очередей и асинхронной стейт-машины поверх них. Например локальный инстанс RabbitMQ + MassTransit Saga. Но тут бизнес-код придётся писать определенным образом, разбивая его на отдельные job-ы.

Спасибо, посмотрел. Как я понял, идея аналогична AWS Flow Framework — программа перезапускается, но обращения к внешним «activity», история вызова activity сохраняется и де-дуплицируется при повторном запуске программы.

checkpoint внутри for это как произвольный goto... хуже антипаттерна

Если нет критических требований по быстродействию (максимальный перформанс), я б подобный код переписал а-ля VM, с псевдоинструкциями. И при сохранении снапшотил данные из ОЗУ и стек псевдо-инструкций. Таким образом, VM сама по себе, и её реентерабельность нам не важна, она стартует быстро, остаётся только псевдо-инструкции помнить => мы реализовали свою ОС и планировщик задач :)

Ещё вариант без VM, это вычисления группировать в блоки-транзакции. Результаты транзакций где то хранить с идентификатором сессии (итерация цикла например, или итерация вызова компонента). Если нужно загрузиться из сейва и продолжить — зная идентификатор сессии, выполнять код и скипать транзакции до первой не вычисленной.

Вот и скотились в IT-религию, где даже элементарнейшие знания пытаются заменить на какие-то верования о паттернах.

ЕСЛИ у процессов есть возможность обозначить чекпоинт, ЕСЛИ задача имеет явное разбиение или хотя бы квантование — ей не нужны твои монстры в гектары памяти на экземпляр (ну пусть даже метров 100 на зверушку если это какая-то микрозадачка) и 20-60 секунд на подъём из гроба из хайбернейта. Программа может вполне стартовать девственницей, равно как и сэйвить чекпоинт исключительно по ключевым данным, а то и вообще выплюнуть сей процесс в отдельный софт.

Ты когда на Gmail письмо пишешь, никогда не подсматривал, что в папочке Draft в осадок выпадает? Собстна, всё. Никаких тебе виртуальных ОС, выделенного ОЗУ, псевдоинструкций. Тупо несколько килобайт в файл и все дела.

checkpoint внутри for это как произвольный goto... хуже антипаттерна

Не согласен. Если мы даем возможность делать чекпоинт, то какая разница где его можно делать? Делать внутри цикла ни чем не отличается от делать его за пределами цикла. Это все переменные, код, указатель инструкций — им все равно цикл это или нет.

я б подобный код переписал а-ля VM, с псевдоинструкциями.

Нет проблемы сделать новый язык программирования или VM, оптимизированные для чекпоинтов. Проблема в том, что этот язык никто не будет использовать. Я хочу сделать так, чтоб использовать такую штуку можно было с помощью мейнстримовых языков, т.е. пользователям не прийдется учить новый язык и технологию.

Делай патч на ядро Линуха)

А шо скажут девопсы за >>500к о состоянии науки и техники по поводу суспенда контейнеров и прочих виртуальных машин?

Девопсы за 500 могут суспендить мир.

Мне видится, что задача сохранения контекста в рантайме должна быть неплохо решена в реализациях «green threads» (Golang, ...) и «event loop» (nodejs, ...).
И если покопаться как устроено, то в момент переключения контекстов, должны быть уже безопасно изолированы стек, регистры, которые можно сохранить во вне.
Проблему открытых сокетов решить сложнее, конечно, но если без этого можно обойтись, то и хорошо. А вот что делать с heap’ом, который может занимать сотни мегабайт и больше, как его быстро сохранять тут хз. Можно посмотреть на тот же Redis с похожей задачей, там 2 способа — rdb (снепшот), fork + copy-on-write memory механизм и aof (сохранение изменений), у каждого свои плюсы и минусы.

Я думаю, что в реальности, в нано масштабах, возможна передача одного бита информации из состояния ПОСЛЕ в состояние ДО некоторого изменения состояния чего-либо.
Теоретически. Это следует из существования и использования нами обратимых функций при моделировании каких-ибо процессов. В условиях замкнутой системы при небольшом количестве параметров можно смоделировать ее любое состояние прохождением по некоторому пути (и в том числе и используя «нить Ариадны» или «хлебные крошки» или «помечая стены», чтобы вернуться в «условное прошлое» системы)
Для меня система во времени это пачка состояний. К каждому состоянию привязан свой идентификатор. Цепочка идентификаторов это путь. Осталось только уточнить, что иеднтификатором может быть и момент времени.

Похоже на то что решается конечными автоматами (finite-state machine)

Как можно этот конечный автомат описать средствами, хорошо понятными простому разработчику без необходимости изучения каких-то новых фреймворков и языков?

Как:
1) Набор переменных (полей) в классе
2) Методы сериализации и десериализации этих переменных

Годный наброс, давно такого в пятницу не было)
Хотя и сама задача любопытная

Тем более)
Жаль только, что много коментов тема не наберет..

Надумал что-то вроде:
При изменении переменных записывать момент времени и характер изменений
( была переменная а=100 стала а=200 в интервал времени т0-т1 )
Сохраняем данные на чекпоинтах. Когда надо восстановить — быстренько пробегаем до необходимого момента времени Х (или поиском необходимый срез получаем)
Но что касается окружения программы — оно ведь по таким правилам не работает. Если бы работало можно было бы и внешнее...

Я понимаю, вы говорите о высокоуровневом подходе, но как это на практике реализовать, когда «переменная» может быть регистром на CPU, например.

Кстати, представил себе некий такой ваш обратимый sand-box с возможностью запуска там Mahjong Titans. В этой игрушке есть такая проблема: безвыходный расклад. Самое обидное, что в начале игры неизвестно, является ли расклад безвыходным или нет. Итого, пытаешься играть честно, пытаешься отыскать алгоритм и вдруг — ба-бах, две одной масти друг на друге.
Вот с помощью обратимых вычислений — прохода по всему дереву возможных решений снова и снова пока не_нахождение_выхода (постепенно отбрасывая неудачные момент времени) — можно потом сообщение пользователю — этот расклад точно можно сложить.
Ведь неприятно постоянно проигрывать один и тот же расклад, особенно не зная — может его совсем нельзя сложить.
Но скорость прямого и обратного полета на такой импровизированной машине времени всех возможных перестановок здесь не так-то и важна — было бы в концов обнаружено, что решение есть.

— Я пытаюсь запилить решение для сложной неординарной задачи...
— О, круто. Если получится, я смогу его использовать для того, чтоб читить в Mahjong Titans!

Технологии сто лет в обед. Дампишь процесс стандартными средствами оси, подымаешь опять и продолжаешь выполнение.
Основные проблемы.
1. Все внешние ссылки. Если что-то изменилось в окружении и программа не смогла это обработать то привет.
2. Таймеры, часы и прочьи недетермееированные штуки.
С простыми программами типа блокнота, такое студенты на лабах в КПИ делают.
Что-то сложнее сделать — очень много неизвестных, читать выше.

Не думал, что это настолько тривиально. У вас есть ссылка на пример как это реализуется?

Корутины? boost::coroutine, boost::context ?

Перечитал. Да корутины тут не помогут.
Но я бы подумал в сторону того как реализована защита приложений так называемой техникой наномитов. Если нужно я когда то реализовывал подобное и могу поделиться питоновскими скриптами и наработками которе есть. Суть там такая, есть процесс-отладчик и есть процесс который запускается под отладчиком. Код процесса который запускается под отладчиком обрабатывается специальным образом, если мы говорим об защите то все инстркции переходов je, jz, jmp.. и так далее заменяются на прерывание на вызов отладчика int 3. Собственно потом отладчик зная откуда кто его дернул и используя сгенерированне таблицы и выполняет установку ip регистра.
Вам же по сути нужно по int 3 процессом-отладчиком нужно сделать дамп отлаживаемого процесса и когда нужно «развернуть» его обратно.
Если я правильно понял. А вот что делать с дискрипторами, то наверно без специальной поддержки со стороны OS (или специального драйвера) ничего сделать не получится.

Там же куча ресурсов ОС, вроде файловых дескрипторов, не будет совпадать.

Я могу добавить ограничение — никаких открытых дескрипторов во время чекпоинта.

Ну вот держи презентаху как такое работает ithare.com/...​-systems-with-transcript
И как его сделать ithare.com/...​ystems-with-transcript/2

Спасибо, очень полезная информация.

То, что вы хотите, есть «из коробки» в image-based языках, например:
— forth (многие реализации)
— sbcl (возможно, другие lisp реализации тоже)
— factor
— small talk
— 
В «мейнстримовом» языке, вам придется делать «закат солнца вручную» — например, реализовывать какое-то подобие форт машины.

Эппл купила разработку и переименовала в M1 :(

Сенкс. Как я понял, идею позаимствовали у AWS Flow Framework и это то что не хотелось бы переделывать.

Workflow составлено из двух компонентов: decider и activity. Decider описан в коде и он условно говоря может «остановиться, и сохранить свое состояние». Activity не имеют никакого сохранения, они выполняются от начала до конца.

Проблема в том, что возобновление работы decider происходит просто повторным выполнением всего его кода до момента, где он остановился в прошлый раз, де-дублицируя вызовы к activity, которые уже были выполнены. Из-за этого decider не может иметь никаких сайт эфектов кроме вызова activity иначе эти сайд эфекты будут выполняться каждый раз, когда мы восстанавливаем работу decider.

Я эту штуку юзал в свое время, она очень сложна для понимания и работы, легко накосячить.

Глянь эту и связанные статьи чувака ithare.com/...​-reactors-via-allocators
Правда, там сериализация возможна только когда актор ничего не делает — соответственно, каждый чекпойнт превращается в посылку себе сообщения со всеми данными.
Также, надо будет всю периферию (и блокирующие операции) запихать в отдельные от логики акторы.

Плюс: почти бесплатный и воспроизводимый реплей логики если сохраняешь все сообщения.
Плюс: логика отрабатывает моментально.
Плюс: забудем про мютексы.
Минус: привет асинхронность и фрагментированная логика на обработчиках событий.
Вот его презентаха про ускорение бекенда таким подходом ithare.com/...​-threading-with-a-script
Вот моя статья, написанная независимо от него (рус) dou.ua/...​cles/telecom-application
Вот после обсуждения с ним разницы наших подходов (англ) hillside.net/...​/2020/papers/poltorak.pdf

Если надо на Джаве — есть Akka Actors. У них, вроде, встроена сериализация и перенос актора между виртуалками. Есть книжка github.com/...​ion.in.Scala.and.Akka.pdf но я пока не дочитал.

Я поскролил статьи но не могу понять суть идеи или какой-то просто пример.

Есть какой-то простой POC пример, по которому было бы понятно в чем суть?

нет простого примера.

суть в том, что сохранить стек, регистры и прочую хрень вместе с маппингом памяти, открытыми файлами и состоянием мютексов — дофига сложно и платформозависимо. но нужно это для двух пунктов:
1) состояние выполнения текущей процедуры
2) работа с периферией
если мы избавляемся от этих пунктов — достаточно просто сериализовать основной объект логики программы, так как его состояние ни от чего внешнего не зависит.

Избавляемся от пункта 1 — переходим от процедурной к событийной парадигме:

class MessageHandler;
class MessageQueue;

class Message {
public:
	virtual void Dispatch(MessageHandler& handler) const = 0;
	virtual ~Message() {}
};

class LogicMessage;
class SerializationMessage;
class MessageHandler {
public:
	MessageHandler(const char* const name) : name_(name) {}
	
	void Run() {	// does not return
		while(true) {
			const Message* const	msg = queue_.Pop();
			msg->Dispatch(*this);
			delete msg;
		}
	}
	
	void Post(const Message* const msg) {queue_.Push(msg);}
	
	virtual void OnMessage(const LogicMessage& msg) {ASSERT(false);}
	virtual void OnMessage(const SerializationMessage& msg) {ASSERT(false);}
	// Other message type handlers
	
private:
	const char* const	name_;
	MessageQueue		queue_;
};


class Serializable {
	virtual ~Serializable() {}
	
	virtual void OnSave(FILE* const file) = 0;
	virtual void OnLoad(FILE* const file) = 0;
};

class SerializationMessage : public Message {
	virtual void Dispatch(MessageHandler& handler) const {handler.OnMessage(*this);}
	
	virtual void DispatchSerialization(Serializable& serializable) const = 0;
};

class SaveMsg : public SerializationMessage {
public:
	SaveMsg(FILE* const file) : file_(file) {}
	
	virtual void DispatchSerialization(Serializable& serializable) const {serializable.OnSave(file_);}
	
private:
	FILE* const file_;
};

class LoadMsg : public SerializationMessage {
public:
	LoadMsg(FILE* const file) : file_(file) {}
	
	virtual void DispatchSerialization(Serializable& serializable) const {serializable.OnLoad(file_);}
	
private:
	FILE* const file_;
};


class Logic;
class LogicMessage : public Message {
public:
	virtual void Dispatch(MessageHandler& handler) const {handler.OnMessage(*this);}
	
	virtual void DispatchLogic(Logic& logic) const = 0;
};

class Action1Msg : public LogicMessage {
public:
	virtual void DispatchLogic(Logic& logic) const {logic.OnAction1();}
};

class Action2Msg : public LogicMessage {
public:
	Action2Msg(const char* const input) : string_(input) {}
	
	virtual void DispatchLogic(Logic& logic) const {logic.OnAction2(string_);}
	
private:
	const char* const string_;
};

class Action3Msg : public LogicMessage {
public:
	virtual void DispatchLogic(Logic& logic) const {logic.OnAction3();}
};


class Logic : public MessageHandler, public Serializable {
public:
	Logic() : MessageHandler("Logic"), count_(0), current_(0) {}
	
	virtual void OnMessage(const LogicMessage& msg) {msg.DispatchLogic(*this);}
	virtual void OnMessage(const SerializationMessage& msg) {msg.DispatchSerialization(*this);}
	
	// Logic interface
	void OnAction1() {
		action_1();
		
		Post(new Action2Msg("Hello World"));
	}
	void OnAction2(const char* const input) {
		count_ = action_2(input);
		
		if(count_)
			Post(new Action3Msg());
	}
	void OnAction3() {
		ASSERT(count_);
		action_3(count_--);
		
		if(count_)
			Post(new Action3Msg());
	}
	
	// Serialization interface
	virtual void OnSave(FILE* const file) {
		fwrite(&count_, sizeof(count_), 1, file);
		fwrite(&current_, sizeof(current_), 1, file);
	}
	virtual void OnLoad(FILE* const file) {
		fread(&count_, sizeof(count_), 1, file);
		fread(&current_, sizeof(current_), 1, file);
	}
	
private:
	void action_1();
	void action_2(const char* const input);
	void action_3(const unsigned counter);
	
	uint32_t count_;
	uint32_t current_;
};


int main() {
	// Create actors
	// Create other actors
	Logic	logic;
	
	// Run actors
	// Run other actors
	logic.Run();	// Never returns
	
	ASSERT(false);
	return 0;
}
Код стал фрагментированным, но состояние программы можно сериализовать посылкой ей сообщения. Как уже писали в теме, если хочется кодить нормально — нужно тянуть корутины, но с ними куча проблем, насколько я понимаю. Сравнение (корутины и без них) есть в моей статье для PLoP.

Избавляемся от пункта 2 — выносим всю работу с периферией в отдельные потоки, которые не нужно сериализовать, а можно перезапустить при старте.

ЗЫ: Вроде как это все есть в Erlang и в Akka «из коробки». Я для быстрого старта юзал code.google.com/archive/p/ting/downloads а потом написал свое.

Сенкс, почитал вашу статью, очень интересно.

ЗЫ2: Если есть бюджет — я этим занимаюсь последние 6 лет, и раньше тоже кусками. Как раз проект заканчивается.

Бюджета нет, планировал пилить простой опен сорс проект.

А что надо сделать? И кому какая польза будет?
Может, его пилить надо не оттуда, или вообще не надо (достаточно выдрать кусок из другой либы).

Хочу запилить возможность описывать асинхронный workflow с помощью обычного языка программирования, как обычную программу. Если я сделаю рабочий прототип сохранения состояния программы, то остальное дело техники (типа интеграция с «взрослыми» workflow engine).

У меня острые приступы PTSD после работы с AWS SWF и AWS Step Functions и хотелось бы по свету доктора запилить что-то, что не повернуто к разрабочтику жопой.

посмотри на TPL Dataflow, может это натолкнет на полезные мысли

Dataflow юзал, не понятно как оно может помочь.

Akka не оно?
Я с облаком вообще не работал и пока не понимаю, что у них есть, и что должно быть.

Асинхронщина всегда неудобна — ты не можешь написать нормальный код. Вариант как писать нормальный код поверх асинхронщины — корутины, но там под капотом очень много, по крайней мере в С++. Возможно, в нормальных языках с батарейками корутины более взрослые, и ими можно пользоваться, не вложив несколько месяцев в написание фреймворка. Тогда — копать в эту сторону. Если корутины используются прозрачно — ты получаешь возможность сериализации/восстановления при ожидании на любой корутине.

Голая асинхронщина — это акторы. Очень быстрое время отклика, но как только доменная логика расползается на несколько акторов — ты сильно огребаешь. И вместо одной нормальной процедуры код лежит в 100500 колбеков, что нельзя нормально дебажить и читать.

Корутины выглядит как интересный вариант, буду копать.

Хочу запилить возможность описывать асинхронный workflow с помощью обычного языка программирования, как обычную программу.

Apache Storm / Apache Flink ?

AWS SWF и AWS Step Functions

Там есть AWS Kinesis Data Analytics, который предоставляет тот же Apache Flink

Действительно, по описанию очень похоже на сохранение состояния актора или сиситемы акторов, с их mailbox-ами. После старта приложения просто поднять все акторы, которые была активны с их состоянием. Не нужно ничего придумывать на стороне ОС. Хотя зависит от задачи наверное. Я бы для начала спросил какая проблема решается.

Интересно. Вы с этой штукой работали? Какой размер дампов получается?

> du -hs .
208K .

На хелло вёрлд на С.

Есть сервис, есть либа. С GUI это работать не будет — в SHM оно не умеет.

А есть языко-платформы, которые чекпоинтами собственно и размножаются. Например CL )

Проблемы тут не столько с сохранением состояния программы (памяти, переменных вот этого вот всего), как с потенциально полной сменой окружения после восстановления из чекпоинта. Открытые хендлы, файловая система и её содержимое, системное время, таймзона, сетевые соединения? Не, не слышал.

Если же писать программу с учётом того, что всё это может поменяться, то это мало чем будет отличаться от сделать/загрузить сейв, ну и зачем тогда городить огород?

Если же не писать, то почему бы действительно не взять виртуалку? Будет дампиться много памяти, да, ну так что ж вы хотели? Каковы констрейнты — такова и цена.

Проблемы тут не столько с сохранением состояния программы (памяти, переменных вот этого вот всего), как с потенциально полной сменой окружения после восстановления из чекпоинта. Открытые хендлы, файловая система и её содержимое, системное время, таймзона, сетевые соединения? Не, не слышал.

Я могу добавить ограничение разработчику — «никаких открытых файлов или сетевых подключений во время чекпоинта»

Если же писать программу с учётом того, что всё это может поменяться, то это мало чем будет отличаться от сделать/загрузить сейв, ну и зачем тогда городить огород?

Если же не писать, то почему бы действительно не взять виртуалку? Будет дампиться много памяти, да, ну так что ж вы хотели? Каковы констрейнты — такова и цена.

Мое предположение было, что можно найти способ как дампить только нужную информацию. Например, в примере, который я написал, все состояние, которое нужно дампить, что б можно было возобновить выполнение, можно уместить в несколько байт.

Размер дампа имеет значение, чем он меньше, тем больше полезных приложений у технологии и меньше ограничений в ее использовании. Дамп несколько байт — его можно делать несколько раз в секунду и хранить в мемкеше. Дамп килобайты — нужно закидывать в какой-то blob столбец. Дамп мегабайты — нужно его делить на части или писать в какое-то файловое хренилище. Дамп гигабайты — это уже труба, только на хранение дампов будет потрачено куча ресурсов, потенциально больше, чем на выполнение программы.

Если ты сможешь внутри сделать что-то вроде flux архитектуры, собрав весь стейт в одном месте, и добавив кучу конвенций что бы приложение могло по сигналу остановиться и сериализавать стейт, то у тебя будет таки маленький объём данных.
Или если упростить, то задача собрать стейт в одном месте и не сделать его баттлнеком, тогда все остальное что ты хочешь становиться простым.

Как я понимаю, C# делает что-то подобное с его генераторами (yield/async/await). Надо что ли поковырять компилятор посмотреть, можно ли этот стейт как-то оттуда вытянуть и сериализировать.

можно дампить всю память целиком, но в каждом последующем чекпоинте сохранять только инкрементальные изменения

Мое предположение было, что можно найти способ как дампить только нужную информацию. Например, в примере, который я написал, все состояние, которое нужно дампить, что б можно было возобновить выполнение, можно уместить в несколько байт.

Но это только локальные переменные. А глобальные, а статические для каждого модуля (и функции впридачу), а состояние подключаемых библиотек? stdlib функции, полагающиеся на внутреннее состояние, типа errno? А так да, установить приложению использование минимума оперативной памяти — вот уже и дампить много не надо.

Размер дампа имеет значение, чем он меньше, тем больше полезных приложений у технологии и меньше ограничений в ее использовании. Дамп несколько байт — его можно делать несколько раз в секунду и хранить в мемкеше.

Смотрим тут ithare.com/...​hreading-with-a-script/3 (Re)Actors with external cache (примерно на 40% страницы)

Решение в лоб — разделить программу на несколько ровно по этим чекпоинтам. Цена решения — вопросы безопасности: тебе на каждом чекпоинте придётся проверить, а не прислали ли тебе левых данных, не хвосты ли это от предыдущих чекпоинтов, не обработана ли уже эта стадия (повторная пересылка вследствие потери пакета или сбоя транзакции). Выгода решения — спокойный сон. Сделал и забыл.

Понадобится скорость — поднимаешь кеширование, начиная от простого файлового кеша, кончая удержанием потока некоторое время (пока по ним сборщик мусора не прогуляется).

PS. Оптимального решения на все случаи жизни нет и не может быть. Время — самый дорогой ресурс в здешних краях, потому исключительно статистикой времени ты будешь определять, что тебе собственно нужно. Но в общем случае тебе нужно хранить состояние оффлайн, потому что гарантий бесперебойной работы системы не бывает — зато есть гарантии что тебя найдут и будут требовать срочно починить, ежели чего мяукнулось и зависло в непонятной фазе.

PPS. Не экономь на логировании. Этим сэкономишь себе туеву хучу времени на отладке процесса. Когда пройдёт год более-менее стабильной работы, львиную долю логирования можешь перенести в дебаг-режим.

Решение в лоб — разделить программу на несколько ровно по этим чекпоинтам.

Как разделить? Если вручную, то это сразу не подходит.

ОС разделяет на точках вызовов в ядро stackoverflow.com/...​y-is-a-cancellation-point

checkpoint обозначает место, где мы бы хотели сделать «чекпоинт» — конкретно, сохранить текущее состояние программы и прервать ее выполнение. При этом процес ОС может быть полностью уничтожен.

Ты ж говоришь что чекпоинты явные? Ну и в чём проблема? Даже если это будет одна и та же программа, но параметрами запуска она может быть тыкнута рылом в конкретный чекпоинт.

Типичнейший пример: игра. Ты играешь, тебе надо сохраниться. А вот продолжишь игру или нет, будет ли это в том же месте где бросил, или вообще из другой страны войдёшь — вилами по воде писано. При этом игра может сохраняться без остановки игры, даже автоматически, равно как и восстановление из сейва занимает секунду.

Факт в том, что сохранение и восстановление — это разные задачи, независимые. И может быть сколько угодно способов и протоколов в твоей дворовой приблуде сложном полигибриде. Но для восстановления тебе придётся реализовать все. В том числе старт программы с конкретного чекпоинта, о котором она либо узнаёт явно, либо явно проверяет какой-либо источник инфы.

Начинаешь реализацию ты с самого тяжёлого и затратного по времени исполнения случая. А уже когда это будет работать, будешь делать оптимизацию под более быстрые (и затратные по ресурсам) алгоритмы. Выгода в том, что на любом этапе ты имеешь готовый продукт, а не идею на палочке.

Підписатись на коментарі