November 12th, 2009

glider

События + транзакции = ?

Сегодняшний пост это ретроспектива написания обертки для DSL Tools. Начал я заниматься этим ибо в оригинальном API зачастую приходится совершить много нелепых телодвижений, чтобы сделать простую вещь, например, открыть файл с моделью. Дело в том, что API хорошо модуляризован, но отдельные части никак не объединены под общим знаменателем, поэтому для целостного юзания приходится писать бойлерплейт. В принципе, авторов можно оправдать, ибо их креатиф предназначался для встраивания в студию, для которой это ненужно, так как крайне вероятно, что разные модули API будут хоститься в разных модулях IDE. Так что нафиг негатив, а я просто решил облегчить жизнь тем, кто вдруг захочет юзать модели вне студии (например, мне).

Один из моментов, которые заставили неплохо задуматься стала реализация ивентов. Конечно, очень хочется в свою обертку добавить события, например, для цивилизованной синхронизации модели с другими структурами данных. Да, и в принципе, в modeling API присутствует понятие "event", так что вроде бы все должно быть гладко. Ан нет.

Дело осложняется тем, что в API присутствуют и другие фичи, которые совсем не ортогонально взаимодействуют с событиями. А именно: undo/redo и обязательная транзакционность (все модификации моделей DSL совершаются только в контексте какой-то транзакции). Как будет показано ниже, из-за этих фич реализация ивентов становится весьма нетривиальной.

Кстати, о дизайне API, перегруженном фичами, не раз писал Эрик Липперт, например, здесь: часть первая, часть вторая, часть третья. Его статьи крайне рекомендую к прочтению - это чрезвычайно эрудированный и знающий человек, да и пишет он очень увлекательно.

Возвращаемся к теме поста.

Добавляя в транзакционный API события, надо, во-первых, решить, когда будут бросаться ивенты: сразу на месте или только после коммита. С одной стороны, если события генерируются сразу по изменению модели, то получаем трабл с возможной неконсистентностью данных, которую увидит обработчик. С другой стороны, если события случаются только после коммита, то мы имеем проблему внесения изменений в модель из ивент хандлеров. Во-первых, эти изменения будут осуществлены уже в другой, вспомогательной, транзакции, что нарушит работу undo. Во-вторых, случись в процессе изменений исключение, отменена будет лишь вспомогательная транзакция, а главная - нет, что нарушит целостность модели. Кроме того, единичный ивент, даже если он и является частью закоммиченной, т.е. консистентной транзакции, совсем не обязан рассказывать о консистентной модели, а это может нарушить работу валидаторов или других хандлеров, полагающихся на корректность данных.

Во-вторых, надо разобраться как будут события дружить с откатами, причем не забыть, что откаты бывают двух видов: rollback и undo/redo. Хорошим гайдлайном на эту тему кажется принцип "если была нотификация о действии, то надо оповестить и о его отмене и возможном перезапуске". Однако не все так просто. В обычном сценарии работы с API возможные сайд-эффекты, продуцируемые обработчиками событий, добавляют дополнительной гибкости и, в общем, желанны. Однако, когда время идет назад или же заново прокручивается вперед, то единственное, чего мы хотим, это перейти в предыдущее или следующее консистентное состояние, а в такой ситуации сайд-эффекты нарушают корректность работы.

После анализа вышеописанных траблов вполне жизнеспособной (хоть и не лишенной компромиссов между универсальностью и корректностью) мне кажется следующая модель (случись что - я отпишусь о возможных косяках).

Обработчики событий делятся на внутренние и внешние. Внутренние обработчики работают только(!) с самой моделью - выполнять сайд-эффекты вовне им запрещено. Логика внешних обработчиков нацелена на данные вне модели, но они также могут изменять саму модель (например, изменение внешних данных триггерит ответ, а он порождает изменения в модели).

Например, хандлер, добавляющий в новосозданный класс интовый primary key по имени Id, - внутренний, ибо ему неважно, валиден ли класс в контексте всей модели. Пример внешнего хандлера - автопреобразователь доменной модели в скрипт SQL; ему-то как раз важно выплюнуть корректный скрипт, а это можно сделать только, если исходная модель находится в консистентном состоянии. Еще один пример - синхронизатор модели и внешней структуры данных: при любых изменениях модели он должен соответствующим образом изменить параллельную структуру, но он также должен быть готов, что эти изменения могут вызвать дополнительные изменения в структуре, которые надо будет распространить на модель.

Итак, вначале о внешних обработчиках.

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

2) Если внешний обработчик желает что-то изменить в модели, то эти изменения проходят по отдельной транзакции. Если она коммитится, то для целей undo объединяется с основной, а если откатывается по ошибке - то вслед за собой тянет и основную.

Чтобы не плодить по транзакции для каждого хандлера, можно создавать одну единственную при начале post-commit событий и коммитить ее же по завершению серии. В DSL Tools это сделать очень легко ибо девелоперы определили события OnEventsBegun и OnEventsAdded. Спасибо им за это! Соответственно, если изменения, внесенные хандлером, снова триггерят внешний обработчик, то триггер сработает только после того, как целиком обработана текущая волна нотификаций.

3) Для того, чтобы выдать внешним обработчикам целостные данные, набор событий, представляющий лог транзакции, компрессируется до тех пор пока не останется ни одной пары связанных событий (например, "Add: a = new A(x = 1, y = 2)" и "Change: a.y = 3" можно объединить в "Add: a = new A(x = 1, y = 3)"). Сжатый лог транзакции генерирует события для внешних обработчиков.

4) С откатом внешние обработчики дружат следующим образом: так как их не вызывают в процессе транзакции, то и во время rollback тоже. С другой стороны, во время undo такие обработчики вызываться должны, но в таком случае мы запрещаем внесение ими изменений модель и предоставляем флажки InUndo/InRedo для адаптации к этому ограничению.

Теперь о внутренних хандлерах.

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

2) Если внутренний обработчик хочет что-то поменять в модели, то он делает это на месте. Проблем с undo/rollback не будет потому, что эти изменения будут логической частью текущей транзакции. Во время откатов внутренние обработчики не вызываются, ибо их единственное предназначение - вносить сайд-эффекты в модель - абсолютно ненужно при играх с временем.

Наконец, как это разделение найдет выражение в API.

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

По факту, девелоперы DSL Tools приняли похожее решение - внутренние обработчики у них называются rules и добавляются только декларативно (любой класс из сборки DSL, помеченный атрибутом, в рантайме автоматически инстанциируется и слушает события модели; извне рулы поступать не могут), а внешние - доступны всем через Store::EventManagerDirectory.
glider

Инфраструктура

Как в свое время меня поразила концепция VCS, выставленного в инет, так же и сейчас я балдею от недавнего апгрейда своей инфраструктуры - переноса базы знаний в OneNote.

Бенефиты, которые приводят меня в восторг:

1. Знания значительно консолидировались в пространстве (рядом лежащие закладки + гиперлинки >> набор несвязанных писем в гмейле), а также на порядок ускорилась навигация между элементами (кто часто юзает гмейл, тот знает, как он иногда тупит), поэтому вполне реально на разогреве за полчаса загрузить область знаний в кэш мозга, а дальше с удовольствием работать.

2. Качественно поменялось время занесения кратких заметок (с 1-2 минут до 10-15 секунд), что очень важно, ибо делаю я это часто. По первым впечатлениям, это позволяет без сброса контекста текущей задачи записывать мысли, не относящиеся к текущей области работы.

3. Весьма ускорилось время поиска по базе знаний, но это важно не до такой степени, так как чаще всего поиск идет по кэшу мозга (см. пункт 1), который уже загружен. Впрочем, отвечать на вопросы коллег линкой из KB, найденной за 5-10 секунд, это прикольно.

Справедливости ради стоит отметить, что уменьшилась доступность KB, ибо теперь ее нельзя почитать с телефона или подъюзать с другого компа. Эти проблемы решаемы через организацию хостинга и скедуленный паблишинг OneNote -> HTML в инете (блин, жалко гуглокодовский svn не прокатил), но нужны дополнительные инвестиции времени.