Как стать автором
Обновить

Комментарии 122

Отличное решение! Вот только как оно будет работать в условиях конкурентной модификации? Скажем, два пользователя пытаются почти одновременно провести документ? Первый получает сообщение «у вас не все цены сходятся», а второй в это время успешно проводит. Первый скажет «всё равно хочу провести» и получит сообщение «документ уже проведён»? А что если условия распространяются не только на свойства документа, но нужно рыться в табличной части или как-то проверять ассоциированные документы? Универсальных решений тут, понятное дело, нет, будет не очень хорошо в любом случае, но всё же хочется узнать, как будет вести себя система с предложенным механизмом?

Кстати, какие есть pros/cons относительно использования генераторов?
Первый скажет «всё равно хочу провести» и получит сообщение «документ уже проведён»?

Да, это неизбежное зло, на которое мы пошли.
А что если условия распространяются не только на свойства документа, но нужно рыться в табличной части или как-то проверять ассоциированные документы?

Метод заново полностью выполняется и пересчитывается, так что он будет оперировать уже новыми данными(теми, которые он видит в транзакции, конечно)
Кстати, какие есть pros/cons относительно использования генераторов?

Вот это не очень понял, если честно
Да, это неизбежное зло, на которое мы пошли.

В любом случае будет неизбежное зло. У меня так вообще получит невменяемое сообщение «не удалось провести» (по неясной причине), и только при следующем нажатии кнопки «провести» получит правильный отлуп.
Метод заново полностью выполняется и пересчитывается, так что он будет оперировать уже новыми данными(теми, которые он видит в транзакции, конечно)

Ага, я это понял. Вот только что если благодаря этим изменениям по-другому валидация пройдёт? Например, идёт последовательная проверка else if'ами нескольких взаимоисключающих условий, и к моменту подтверждения пользователем валидации, сработавшем на первом условии, кто-то приведёт документ к такому состоянию, что сработает уже второе условие. С точки зрения пользователя это будет абсурд. Хотя, вероятность такого ничтожно мала.
Вот это не очень понял, если честно

Ну вот как-то так:
   public IEnumerable<Interactive> KillAllHumans(Human[] humans)
   {
       foreach (var human in humans)
       {
           if (human.Name == Context.Current.User.Name)
           {
               yield return new Confirmation(guid, "Вы были обнаружены в списке человеков. Все равно убить?");
           }

           human.Kill();
           yield break;
       }
   }
Ну вот как-то так:

У нас такие действия возвращают много чего, не только подтверждения. Фактически, мы можем делать все что угодно с клиентом из такого действия, с помощью вот такого:
public void Execute(IFormContext<Order> context)

Внутри context как раз и есть методы, для управления клиентом и показа всяких штук. А вот данный метод как раз используется для неявного прерывания дествия.
А кто мешает в Interactive заворачивать функцию?
Типа такого?:)
public static void Interactive(Guid id, Action method, string message, string header)

А в чем проблема, собственно? Правило «Кто первый встал — того и тапки» работает — а больше ничего и не нужно.
Отличная статья! Очень интересно
А ещё можно избавиться от явного задания GUID-ов с помощью модификации байт-кода.
Отличная идея, спасибо:) Только у нас и так постшарпа довольно много, думаю что такое решение довольно прилично замедлит сборку.
Моё решение проблемы (часть функции WModalWindow.show):

  selfSession.afterHandleRequest();
  selfSession.sendResponse;
  selfSession.modalSleeping := true;
  while selfSession.modalSleeping and not selfSession.isFinished do
    selfThread.Handle;
  selfSession.ModalSleeping := true;

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

selfSession.modalSleeping := false;

Преимущества решения:
1. Работает, не глючит.
2. Работает с иерархическими модальными окнами (модальное окно внутри модального окна....).
3. Предоставляет удобное API, абстрагированное от этой проблемы

  if web.ModalDialog('Warning', 'Delete all files?', WTQuestion, WBYesNo) <> wmrYes then
     halt;

задача: web ERP
serverside: delphi 7, WinAPI;
clientside: js, extjs
Ух ты, а можно где-нибудь глянуть на клиент. У меня неудержимый профессиональный интерес.

P/S Когда увидел := аж всплакнул, настальгия, эх…
Боюсь, что пока нельзя. Но как только оно заведется — сделаю статью, на тему основных проблем, покажу внешний вид.
Отныне я слежу за вашей активностью на хабре >:-]
А чего с транзакцией? Так и будет висеть, пока пользователь не разродится ответом?
Да. И тут нет проблем, так и должно быть согласно бизнес логике. Ну разве что, сессия в течении 15 минут запустит selftermination если пользователь не будет ничего делать.
А лично меня как-то пугают длинные транзакции. Ведь транзакция может заблокировать какой-нибудь ресурс, а пользователь банально ушёл попить кофе или просто сессия отвалилась.
Ну на то она и транзакция. что-то сломалось — не страшно.
А глобально, если честно, я не уверен что полностью понимаю суть транзакционности в данном контексте.

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

Я в этом контексте лишь предоставил им инструмент модальных окон, которые они могут дёргать со скриптовых языков.
Это нормально. Такой паттерн называется Session-per-Conversation, а сам подход «Database Conversation» и при правильной реализации является мощным инструментом. Обработку правильных таймаутов никто не отменяет.
Подозреваю, что в указанной ссылке говорится не о транзакциях БД, а о т.н. бизнес-транзакциях, которые бывают размазанными по нескольким физическим транзакциям. Штука, как мне кажется, несколько небезопасная, т.к. в этом случае бизнес-метод содержит прямо в переменных данные, которые могут немного устареть.
Ну по ссылке говорится о конкретной реализации этих паттернов в Hibernate (на Java), но суть от этого не меняется. В целом — это тема реализации паттерна Unit of Work, который нужно реализовать, исходя из требований к проекту.

Любые данные, введенные пользователем, уже устаревшие — вопрос лишь в том, насколько, и насколько это приемлемо для системы.
У меня такое ощущение, что вы с автором друг друга не поняли. Судя по описанному выше коду, транзакция будет висеть и после ввода пользователя — стартанёт новая. Если где-нибудь вокруг ExceptionHelper'а её явно не закрыть (можно несложно допилить метод и передавать туда делегат, который вызовется если ответа пользователя ещё нет в БД; но тогда у программиста пропадёт иллюзия того, что его код вызывается 1 раз). Так что мне тоже очень интересно узнать, а как тогда предлагается бороться с транзакциями? Из примеров кода догадаться (мне) нереально :(
Я так понял, что при показе окна диалога пользователю первая транзакция откатывается. В случае подтверждения, стартует вторая транзакция, которая повторяет один-в-один первую и продолжается дальше. И если ничего плохого не происходит, успешно коммитится. Автор раскрыл общую идею, но не показал кода, в который всё это оборачивается. Но интуитивно мне кажется, что там скорее всего using на сессию/транзакцию, и при любом исходе сессия хоть как-нибудь, да прибивается, пусть даже и с откатом транзакции, если что-то пошло не так. Может быть даже после вызова основного метода транзакция автоматически коммитится, так что откатить её можно, выкинув исключение, а в любом другом случае она утверждается.
Спасибо:) Видимо, я и правда не сильно хорошо описал процесс, что половина читателей просто не поняла, в чем суть:)
Ваше решение противоречит требованиям 2 и 4, которые я укзал в начале статьи. Изначально мы примерно так и делали.
У меня действия пользователей независимы абсолютно. Блокирования потока не приводит к блокированию других пользователей. У меня сложный event loot.

Реквестирую детальное формальное разъяснения пункта 2.
и более того. Блокировка потока, не приводит к остановке обработки событий в этом потоке, оно просто переносится в другое место callstack-а, там же написано)
Вообще-то, не очень хорошо открывать транзакции и делать всю бизнес-логику прямо в формочках. Для этого делают отдельный бизнес-слой. На Delphi с этим не заморачивались. Но даже если мешаем бизнес-логику с представлением, всё равно надо думать о том, что открытую транзакцию нельзя держать по 15 минут. Транзакции надо завершать как можно раньше. Иначе страдает конкурентный доступ к БД. Опять же, для времён Delphi, когда одной такой программой пользовались 3,5 человека, было не страшно. А тут же веб-ERP.
1. В моем проекте бизнес логика имеет отдельный концептуальный слой. По сути, вся эта комплексная информационная система является неким (framework?) для разработки бизнес логики.

Также бизнес логика, имеет отдельный слой в client side, а также server side.

2. Если вы про транзакции СУБД, то это уже другой вопрос. И думал о транзакционности всё той же бизнес логики.

3. Оно работает с тысячами, без проблем.

P. S. Прошу, давайте уточнять неясности, мне кажется что вы обвиняете мое решение в том, в чем оно невиновно
Да, насчет 2 я ошибся, конечно
Можно небольшой вопрос по интерфейсу? А что именно делает кнопка «Отмена» в диалоговом окне на первом снимке экрана?

На Хабре уже было обсуждение про это. Смысл в том, что такую кнопку часто отображать не стоит, а если отображать, то пробовать другие названия. Лучше всего, чтобы она более детально описывала конкретное действие. Потому что сейчас непонятно: то ли эта кнопка закроет диалоговое окно (что сомнительно), то ли удалит из корзины оба товара, то ли удалит тот, что был добавлен в последнюю очередь, то ли наоборот удалит тот, который был добавлен сначала.
Видимо, немного неудачно выбрал фразу для вопроса, точнее не формил ее в виде вопроса. Если вы посмотрите пример, думаю, вам станет все понятно
Надо наверное картинку подрихтовать)
Статья как-раз про диалоги в духе «идёт убийство всех человеков. 42 из 7000000000 убиты», прогрессбар и кнопка «отмена». А про диалоги, что в первом снимке, говорит, что всё нормально.
Вообще-то, исключения — они немного для другого сделаны. Не надо их использовать для _условного_ управления потоком выполнения.

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

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

Именно этого и нет, о том и речь, что мы ничего не блокируем
«Согласен, но в нашем случае это удобный способ остановить выполнение, откатить все изменения и показать нужные нам данные.»
Это называется bad design.

«Именно этого и нет, о том и речь, что мы ничего не блокируем „
Значит, у вас не транзакция. Потому что чтобы была транзакция (в части C из ACID), вам надо заблокировать те объекты, которые были до запроса пользователя. Иначе в вашем
— СложнаяПроверкаРасчетДанных();
если (данные.НеСовсемВерны())
{
СпроситьПользователя(“У вас дебет с кредитом не сходится, продолжить?”);
}
ПродолжаемОбработкуДанных(данные);
— данные между проверкой и обработкой изменятся.
Это называется bad design.

А как же велит поступать good design?
данные между проверкой и обработкой изменятся

Да нет, всё будет нормально. Будет две транзакции. Первую откатят, словив исключение. Откроют вторую, повторят, что было в первой, но продолжат выполнение дальше, не возвращая никаких статусов пользователю.
про второй случай возможна ситуация:

Исключение — вы превышаете лимит операции на 10 рублей, продолжить?

в этот момент, другой пользователь добавляет каких-то данных, так что лимит начинает превышаться на 100 000 рублей.

При этом, после нажатия на продолжить, система не обратит внимание на то что лимит уже превышен не на 10 а на 100 000.
Такое, к сожалению, никак нельзя побороть. Я уже писал об этом выше. Ситуация немного (нефатально) нехорошая, но никак неизбежная.
В данном случае мы можем генерировать идентификатор исключения e1 хитрым образом, например:
var text = string.Forma("Лимит превышен на {0} рублей", summ);
var e1 = someGuid.CombineWithHashOf(text);
ExceptionHelper.Interactive(e1, text);

Где CombineWithHashOf преобразует каким-то хитрым образом идентификатор исключения для учета сообщения о превышении.
На практике обычно бывают более сложные правила валидации. Если система будет выдавать по 10 сообщений, мол «извините, пока вы думали, я теперь по-другому этот документ валидирую», то у пользователя будет WTF-реакция. Вообще, если превышение было хоть какое-то, но не фатальное, требуется вмешательство пользователя. Если же превышение было, скажем, более чем на 1000 рублей (или другой установленный бизнесом лимит), то система вообще должна сказать «фатальное превышение, не буду проводить». В крайнем случае, можно для различных градаций (10, 100, 1000) писать три валидации подряд. Хотя, опять же, будет лёгкий WTF у пользователя: только что мне система сказала, что превышение было на 15 рублей, а теперь опять спрашивает, но уже называет 123 рубля.
Побороть — можно, и очень просто: каждый раз начинать операцию сначала, а не с неявной «точки прерывания».
Она и начинается сначала. Вот только это не помогает, т.к. небольшие артефакты в редких случаях всё-таки возможны.
А поточнее? Если транзакция атомарна, и все проверки производятся в ней же — то какие артефакты возможны?
Ну как я уже много раз говорил — в очень редких случаях пользователь будет получать несколько неадекватную с его точки зрения последовательность сообщений. Конечно же, на целостности данных это не скажется.
Как это неизбежная? Атомарно проверять данные каждый раз и ни шагу в сторону. Один раз команда не сработала — выкинуть ее, указать пользователю на ошибки, отправить на выполнение новую команду. Если и второй раз не сработала — так тому и быть — опять уведомить пользователя. Это редкая ситуация, что пользователь будет дважды править данные, для которых провалилась Model-Level-валидация, но мы гарантируем целостность данных.
Так про целостность данных никто и не говорит — её-то можно обеспечить. А вот небольшой WTF со стороны пользователя в редких случаях, всё же будет.
Величина WTF пользователя зависит от вероятности возникновения таких ситуаций. Но, в общем случае, на практике, целостность данных важнее.
Конечно, важнее. Она будет сохраняться. Просто WTF в редких случаях неизбежен, вот и стало интересно, как именно он будет проявляться.
За такие ситуации надо руки вырывать прикладникам и перешивать на те места, откуда они должны расти у Разработчиков. Проблема надуманная, является следствием говнокода, в частности некорректного использования инструмента.
«А как же велит поступать good design?»
Не использовать исключения для (нормативного) управления потоком выполнения.

«Будет две транзакции. Первую откатят, словив исключение. Откроют вторую, повторят, что было в первой, но продолжат выполнение дальше, не возвращая никаких статусов пользователю. „
А как гарантировать, что данные между первой и второй транзакцией не менялись? (не блокируя их при этом)
А как гарантировать, что данные между первой и второй транзакцией не менялись? (не блокируя их при этом)

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

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

Для этого делаются дополнительные проверки, окнечно, еще до того, как начать что-то делать
Для нормативного — нет, но ошибки Model-level-валидации вполне можно сделать исключениями.
Не критикую, а делюсь опытом: очень советую прочитать пост про Domain Events. Там объясняется, как можно сделать то же самое, но без исключений, а на событиях.
Можно уточнить, а вы вот об этом говорите?
>> // указание выбросить исключение с запросом

Тогда да, это «flow control», исключения выглядят некрасивым решением.
Да, я говорю об этом.
<рецензия>
Боюсь что автор так и не сумел объяснить суть проблемы, но уверяю вас, проблема колоссальной сложности, и неаккуратные решения могут быть причиной быстрого роста энтрапии.

Автор решил проблему гибким способом, что свидетельствует об очень высоком профессиональном уровне, и не смотря на наличие «запрещенных» приемов (речь о нарушении прямой последовательности колл стэка), решение является красивым, удовлетворяющем все поставленные требования, и таким что не нарушает информационную целостность подсистемы (управления бизнес логики?)
</рецензия>
В общем, респект).
Ого, спасибо :)
А я правильно понимаю, что если форма заполнена с несколькими ошибками, то она вместо того, чтобы сразу высказать все претензии, будет выдавать их по одной, да каждый раз запускать обработчик с самого начала?
Всё зависит от. Иногда пользователю действительно нужно показать несколько сообщений подряд. Иногда (например, когда подозрительно заполнена табличная часть), нужно вначале обойти её целиком, составить список сообщений, а потом показать пользователю. Для того механизма, что представлен в статье, это не слишком принципиально. Это уже ответственность другого механизма, который умеет собирать пачкой сообщения валидации.
Ну, в простейшем случае — да. Но никто не мешает сделать разработчику так, например:
var errors = (from o in context.FormData where o.Value != null select string.Format("Error {0} must be null", o.Key)).ToList();

            if (errors.Count > 0)
            {
                ExceptionHelper.Interactive(Guid.NewGuid(), "У вас тут ошибочка вышла" + errors.ToString(", "));
            }

Использовать исключения для чего-нибудь кроме исключительных ситуаций — нонсенс. Задавать пользователю вопросы внутри транзакции — еще смешнее.

Такие вещи нормальными людьми делаются через серверные конечные автоматы (aka workflow aka stateful services).
Ситуация в некотором роде исключительная — мы наткнулись на подозрительно заполненные данные и не знаем что делать. Может, концептуально было бы правильнее использовать генераторы (хотя, например, в Java таковых нет). Но с ними и код потяжелее отлаживать, и немного странновато сигнатура метода начинает выглядеть. А вопросы внутри транзакций не задаются. Они задаются между транзакциями.
Это не исключительная ситуация, это вполне обычная валидация.
Если валидация делается на клиенте, то валидация, с гламурными errorprovider'ами и подробными разъяснениями. Если на сервере — то любые невалидные данные должны давать отлуп. В принципе, я у себя так и делаю: серверная валидация проверяет данные на валидность, и если что, просто возвращает ошибку. При этом не делает никаких предположений вроде «у вас в строчке 23-й плановое количество отличается от фактического». Все такие вещи делает клиент, исключительно над теми данными, которыми он располагает, пусть они и несколько устаревшие.
Согласен, вполне правильный подход.
Правильный, но порождает небольшой оверхед за счёт необходимости делать проверки на жёсткую корректность (например, попытка провести уже проведённый документ) как на стороне клиента, так и на сервере. Тут по сути похожее решение. Можно это представить как будто на сервере живут вспомогательные методы хинтинга. Дёргается метод хинтинга и если он прошёл успешно, то метод, непосредственно выполняющий действие. Кстати, если смотреть с концептуальной стороны, то правильнее было сделать так: хинты, которые мы применяем или не применяем, должны храниться не в базе, а передаваться каждый раз клиентом.
генераторы в принципе и есть простой способ соорудить конечный автомат
Автоматы убивают читаемость кода, в контексте бизнес логики. Логическая последовательность связанных действий (функция) — перестает быть функцией, потому что ДКА видели те появились.

Это вопрос, который стоит на обсуждении во многих НИИ, а вы так просто говорите что нормальные люди автоматами это делают.

Я, честно говоря, думаю что новая версия продукта, который я разрабатываю — будет асинхронной, с иерархической организацией ДКА. Но и тут многим жертвовать нужно, и только для производительности все это.
Да ладно? Windows Workflow Services без xaml'а видели?
И всегда можно придумать маленький dsl`чик. State Machine в рельсах видели?
Да, без шуток. Обе эти штуки не трогал.

В смысле, я подозревал что должны были бы быть (какие-то автомато-ориентированные фреймворки?), но все-же хотелось бы посмотреть на крупные приложения (ИС, ERP), которые основаны на основе данных императивов. Они не столкнуться с проблемой блокировок, многопоточности и прочего, но там же другие проблемы появляются, которые мало изучены.
Я вот примерно таким на работе и занимаюсь (большой распределенный документооборот), используем Windows Workflow Foundation (это и есть автоматно-ориентированный фреймворк, даже с визуальным редактором воркфлоу и несколькими вариантами их кодирования/представления) и Windows Communication Foundation. Показать к сожалению не могу, но могу порекомендовать почитать книжки Томаса Эрла про SOA и про WWF.
Принято, спасибо.
А как вы будете их задавать «из вне транзакции»? Если вы сперва сделаете проверки, потом в отдельной транзакции проведете операцию, то при многопоточном приложении между этими двумя состояниями может пройти изменение. См. коммент

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

Или я что-то не понял?

Но самое главное что я не понял:
Такие вещи нормальными людьми делаются через серверные конечные автоматы

Вроде все слова знаю, а смысл не уловил. И что за новое изобретение СЕРВЕРНЫЕ конечные автоматы?! Гугл дает единственную ссылку на эту статью.

Имелось в виду автоматное программирование на сервере, там ниже в комментариях есть кейворды, которые уже гуглятся.

Можно попробовать начать читать здесь — msdn.microsoft.com/en-us/library/aa478972.aspx
Понял. Не подумайте, что я придираюсь, но и упоминание ПРОСТО конечных автоматов мне не совсем понятно.
Если не вдаваться "в тонкости", то любой код по факту детерминирован, и может быть выражен конечным автоматом. И этот — не исключение.

Наверно вы имеете ввиду какой-то конкретный подход к решению этой задачи? Не могли бы вы описать, например, для приведенного выше примера с заказом?
Действительно, любопытно.
Так просто без примера описать сложновато. Поискал по хабру статьи по теме и не нашел, поэтому, если успею подготовить в разумные сроки, попробую ответить вам статьей.
Если в процессе выполнения атомарного бизнес-процесса произошли какие-то изменения — то бизнес-процесс надо откатывать назад в любом случае (так как более ранние решения пользователя были основаны на еще старом варианте информации). В таком случае все равно задавать вопросы пользователю до его выполнения или во время — неважно.

Иначе — это не атомарный процесс и не транзакция ни разу.
Ну и да, гугл на «серверные конечные автоматы» выдает кучу ссылок по смежным темам. А уж на «workflow» и «stateful services»…
Не очень понял требование 2. Можете объяснить, чем оно обусловлено?
Другими словами, почему нельзя разбить код на несколько независимых обработчиков?
Потому что списание средств со счета не должно произойти, если вдруг случилось «что-то».
Разбиваем код на 2 части: проверка на «что-то» и собственно транзакция.
По запросу пользователя выполняется первая часть, если все ок — вторая. Если «что-то» случилось — спрашиваем пользователя, по результатам — выполняем или не выполняем вторую часть.

Чтобы это продолжало быть «stateless», можно не разбивать на две части буквально, а просто положительный ответ пользователя будет вызывать ту же транзакцию с флагом «force».
Данные будут для проверки и транзакции разные. Делая проверки и сами проводки в одной транзакции мы точно уверены, что все проверки прошли правильно.
Кто мешает передавать с клиента одни и те же данные?
омг. На клиенте вообще может не быть данных, в процессе расчетов могут быть использованы тысячи записей, которые через секунду могут быть изменены уже.
А они с клиента и будут приходить одинаковые. Но они будут приходить на неодинаковые состояния сервера, причём в одном состоянии сервера данные от клиента окажутся вполне себе валидными, а в другом состоянии — нет.
Тогда в любом случае это уже «statefull» поведение и делать его соответствующе надо — сохраняя параметры транзакции куда-нибудь на время «ожидания» ответа от пользователя.
А они и сохраняются «куда-нибудь» — в базу, насколько я понял.
По сути, предложенный подход так и делает, только прозрачно для среднего программиста. Программист не заморачивается такими делами, а пишет как есть. Если его научат «выдавать сообщения об ошибках вызовом такого-то метода», он так и будет делать. Единственное, что действительно тут правильно озвучено, что флжок force должен передаваться явно, а не храниться в БД.
Программист должен понимать, что именно происходит в коде. Если в коде должен быть какой-то ввод данных от пользователя — лучше это делать явно.
К тому же, получается логичное разделение логики, простите за тафтологию: отдельно <s&gtмухи</s&gt проверка, отдельно собственно транзакция.
Насчет «force» — я бы не стал так делать вообще.

А вот проверка (валидация) одним шагом, а затем попытка транзакции — опасная штука, но иногда случаях может быть применима. Особенно, если мы уверены, что транзакция «упадет» при попытке провести ее.

Для целостности же данных, я бы спал спокойнее, если все происходит в рамках одной транзакции.
Я уже понял, что речь идет не о stateless-приложении, несмотря на громкий заголовок. И в этом контексте уже совсем непонятно, зачем автор предлагает такое решение.
Ну, заголовок, может и правда громковат, но по факту состояние формы/бд и т.д. нигде не сохраняется между запросами
Состояние формы нет, но состояние «валидации» же сохраняется?
Нет, валидация производится заново, в том и фишка
Ну, исключение, состояние команды, или еще что-то, привязанное к пользователю, сохраняется?
Ну, да сохраняются пропущенные вопросы, но суть в том, что мы не сохраняем состояние выполняющейся операции
При этом, если при первом проходе возник вопрос к пользователю и пользователь ответил «продолжаем», а при втором проходе снова что-то пошло не так по тому же самому вопросу, но из-за других причин — все равно транзакция пройдет? Или пользователю еще раз вопрос зададут?
Предположим вы разобъете метод М1 на две части — Ф1 и Ф2.

Ф1 — проверяет данные, Ф2 — сохраняет их.

Тогда между вызовом метода Ф1 и Ф2 есть некоторое время, в которое другой пользователь может так же выполнить какие-то действия, которые сделают данные, сохраненные в Ф2 некорректными.

Предположим работает 2 пользователя: П1 и П2. Оба они запускают метод М1 (который разбит на 2 части, Ф1 и Ф2).

Пользователь П1 проверил свои данные методом Ф1, проверка прошла. Сразу за этим пользователь П2 проверил свои данные методом Ф1, у него тоже все Ок. Далее пользователь П1 сохранил данные методом Ф2, его данные валидны. А вот когда то же начал делать пользователь П2, произойдет что-то плохое, ведь П1 изменил данные, соответственно П2 сохраняет методом Ф2 уже не то, что проверила его проверка Ф1.

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

Вообще это основы основ работы с данными, и как бы суть транзакционности.
Тогда я не очень понимаю, что автор называет stateless-приложением.
В описанной ситуации нормальным решением было бы полноценное stateful-поведение:
сохранение параметров транзакции где-либо -> запрос к пользователю -> собственно транзакция с сохраненными параметрами
Автор предлагает следующее: У него есть метод М1, в котором грубо говоря есть две части Ф1 и Ф2.

Ф1 — проверяет данные
Ф2 — сохраняет их

Метод М1 обернут в транзакцию. Выглядит это как-то так:

метод М1()
{
НачалоТранзакции();
Ф1();
Ф2();
КонецТранзакции();
}

Предположим пользователь П1 вызвал метод М1. В проверке Ф1 у него возникло исключение, транзакция откатилась, никакие данные не сохранились, а пользователю пишел вопрос «Хотите продолжить, или отменить». Он жмет допустим продолжить и передает дополнительно к вызову идентификатор исключения, которое свалило выполнение в последний раз. Теперь исключение по этому идентификатору будет пропускаться, и таким образом метод отработает от начала и до конца, отработает Ф1 с проверкой и выполнится Ф2. После этого транзакция успешно завершится.

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

Вот если на пальцах то, что предложил автор.

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

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

Как это работает сейчас:

Пользователь П1 начал транзакцию, блокировав данные, П2 тоже начал транзакцию, но которая ожидает пользователя П1 (он что-то блокировал, что нужно транзакции П2). У П1 в методе Ф1 выплюнулось исключение, откатилась вся транзакция, данные освободились. Соответственно П2 может провести теперь свою транзакцию и выполнить Ф1 и Ф2 методы. А пользователь П1 имеет достаточно времени подумать продолжать ли ему выполнение своей операции, или же отменить её. Как описано выше — он ничего не блокирует.

Если бы запрос исключения был бы асинхронный, а его транзакция не откатывалась, а просто приостанавливалась, висела в памяти, то данные были бы блокированы значительное время. Пользователь П2 должен был бы ждать пока пользователь П1 пообедает, выпьет кофе, посидит на хабре, и после всего этого нажмет «продолжить» в своем исключении.

Вообще ситуация сложная и в деталях выглядит ещё сложенее, т.к. могут применяться разные уровни изоляции. Это простой пример как это работает, и почему так, а не иначе.
Асинхронный = неблокирующий.
Строго говоря такого соответствия нет. Это все равно что сказать мягкое = теплое.

Потоки могут блокировать друг друга, ожидать ресурсов, выполнения других потоков и т.п.

Вот то что описано в статье — это с какой-то точки зрения и есть асинхронное взаимодействие с данными, относительно данных. Т.е. операции над ними выполняются не синхронно, в порядке запроса, а асинхронно в порядке готовности к выполнению.

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

Вообще, когда возникают подобные проблемы, у меня стойкое ощущение, что что-то сделано не так. Но подробностей я не знаю, поэтому сильно спорить не буду.
Тут мы углубляемся в то, что реализуется с помощью очередей команд и все такое прочее :)
А можно немного подробностей про само устройство системы. Вы обозначили, что это «Web ERP». А как именно происходит информирование пользователей? То есть «В неподтвержденную форму клиента пришел запрос Да/Нет.» — каким образом? Кто инициировал передачу?
Ну, по сути, конкретно то, что я тут описал используется исключительно для user-initiated действий. Push уведомления мы не реализовывавали пока, за ненадобностью.
Тут надо бы дать рекомендацию максимально переносить проверки в начало кода, чтобы не оказалось, что после ста тысяч вставки транзакция будет отменена по причине, которую можно было проверить В НАЧАЛЕ кода.
По-моему вы изобрели конечный автомат, применение которого в подобных системах реализовано в виде классной штуки под названием NServiceBus. Нет? Длинный процесс разбиваем на шаги и либо дожидаемся его завершения, либо ловим событие\сообщение о необходимости вмешательства пользователя и выводим ему соответствующий MessageBox. По результату закидываем ответ пользователя в уже начатый процесс/автомат и он продолжает свою работу.
Прочитайте внимательно, мы ничего не ждем, никакие процессы не останавливаем.
См. коммент ниже, я видимо не на ту кнопочку нажал.
Ну дык поэтому и спросил, увидев в самом начале: «При этом в строке подтверждения нам бы хотелось, чтобы произошла магия: Код приостановил работу.»
Потому и «магия», что для разработчика это вылядит именно так, будто код остановил работу, а на самом деле это не так:)
Дык! Решение довольно необычное и интересное, но всё же, имхо, довольно узкоспециализированное. (В Интернет-магазине скорее всего сработает.)
Мы с вами оба понимаем, что на самом деле код, если он хоть раз потребовал ввода пользователя, будет выполнен не один раз. Но благодаря «магии», программист, использующий ваш «фреймворк», об этом задумываться не будет. И самой первой строчкой метода запомнит в свойствах генерируемого объекта текущее время. А затем будет очень долго дебажить, почему начав бизнес-транзакцию в 11:00:00, в отчёте фигурирует время 11:01:17! Или перед вводом пользователя обновит пару полей в БД (допустим, постепенно формируя заказ и по ходу спрашивая пользователя какие-то уточнения). В итоге, можем получить неконсистентные данные. Как раз потому, что программист не задумался о том, как же на самом деле «магия» работает.

А те же конечные автоматы в целом и NServiceBus в частности, хотя и (как где-то выше верно подметили) усложняют первоначальное восприятие кода, но зато отлично защищают от незнания разработчиком общей архитектуры разрабатываемого его командой ПО. В данном случае это намного важнее синтаксического сахара.
А если «прерываемый» код — это парсинг какого-нибудь загруженного извне XML-файла на 200Мб (который при простом открытии кушает 1Гб оперативной памяти и требует на эту операцию десяток-другой секунд процессорного времени), то вообще привет… :(
Ага, а потом потребуется не предупреждение, а селект из трех вариантов. А потом к нему еще чекбокс «запомнить на будущее». Жду развернутый отчет на хабре, как вы снова и снова успешно победили одну и ту же проблему :)

Вы изнасиловали священную простоту REST а на серверной стороне поселили личинку то ли брокера то ли шины, которая по ходу проекта вырастет и даст всем просраться.

Сообщение не живет отдельной жизнью. Всегда можно сформировать запрос на валидацию ввода и серверный хэндлер к нему в одном случае или сообщить что операция породила проблемы и дать возможность ее отменить. Последний вариант к тому же способствует неделимости транзакции и пониманию того, как транзакции с их откатами должны работать.
+1
Зарегистрируйтесь на Хабре , чтобы оставить комментарий

Публикации

Истории