AXForum  
Вернуться   AXForum > Блоги > CRM, SharePoint и Черная Магия
All
Забыли пароль?
Зарегистрироваться Правила Справка Пользователи Сообщения за день Поиск Все разделы прочитаны

Добро пожаловать в мой блог! Изначально он не задумывался как блог CRM разработчика, но жизнь сама внесла нужные коррективы. Тут я публикою все свои наблюдения относительно обозначенных в заголовке систем. Если Вы найдете в нем что-то интересное для Вас, как для заказчика, то буду рад сотрудничать с Вами! В моей компетенции 100% задач по MS CRM 3.0/4.0/2011:
  • Консалтинг
  • Проектирование
  • Разработка
  • Обучение


MVP 2010, 2011
Оценить эту запись

Как работает условие ожидания в асинхронном сервисе

Запись от Артем Enot Грунин размещена 09.02.2017 в 18:59
Теги async, bug, workflow

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

Как выяснилось, ожидающие процессы тоже управляются событиями! Детали этого механизма будут раскрыты дальше в статье, однако, на случай если вам жалко времени, изложу его сперва кратко:
1. Шаг ожидания содержит в себе условие, при выполнении которого процесс должен пойти дальше
2. На основе этого условия система создает "подписку" на все изменения нужной записи
3. При изменении записей система проверяет есть ли у нее процессы-подписчики
4. Если такие процессы есть, система изменяет их состояние на "запущен"
5. Далее рабочий процесс запускается и выполняет проверку шага ожидания
6. Если условие выполнилось - подписка удаляется и процесс идет дальше. Если нет - "засыпает" снова

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

Немного истории. Увы, не могу вспомнить какие-либо официальные источники об архитектуре рабочих процессов и асинхронного сервиса в целом. Мне встречалась только одна статья, более или менее раскрывающая данный вопрос: https://blogs.msdn.microsoft.com/crm...pend-or-retry/, но, к сожалению, она раскрывает не все детали, или устарела (2009 год!!!). Все что мы можем знать, мы знаем на основе догадок и понимания технологий, которые лежат в основе системы.

Мы знаем, что определенные операции в системе порождают записи в таблице AsyncOperationBase. Атрибут OperationType определяет тип операции: рабочий процесс, плагин, или что-то еще. Серверы системы, где развернута роль асинхронного сервиса, каким-то образом мониторят список заданий и распределяют их между собой для выполнения. Далее каким-то образом запускаются механизмы Windows Workflow Foundation, которые инстанцируют тело процесса из его определения в XAML (которое хранится в таблице WorkflowBase), каким-то образом инициализируют его состояние (которое хранится в атрибутах Data и WorkflowState таблицы AsyncOperationBase), после чего Фреймворк выполняет шаги процесса.

Так как все-таки работают ожидающие процессы? Чтобы ответить на этот вопрос, мне пришлось сесть с включенным SQL Profiler и разобраться. Очевидно, я не претендую на истину последней инстанции, так мой реверс инжиниринг был вполне посредственным и затрагивал только протекание конкретного процесса с конкретным условием. Как бы то ни было, мне стало немного понятнее как работает система, и, надеюсь, будет полезно кому-то еще.

Предположим, есть процесс, который стартует после создания Сделки, создает Задачу и ждет пока она не будет выполнена. Что будет происходить в системе, когда пользователь создаст Сделку?

Шаг 1. Запуск задания процесса. Каким-то образом система понимает, что у события есть обработчик и создает соответствующую запись в AsyncOperationBase. Исследование этого механизма не было моей основной задачей, так что опустим подробности как именно это делается.

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

Шаг 2. Создание подписки. В какой-то момент, процесс доходит до шага - ожидания и проверяет условие. Так как условие еще не выполнено, создается запись в таблице WorkflowWaitSubscriptionBase - это и есть наша подписка. Запись содержит:
• Ссылку на асинхронную операцию в AsyncOperationBase
• Идентификатор и тип записи которую "ждет" процесс - в нашем случае task.
• Имена полей через запятую, изменения которых "ждет" процесс - в нашем случае statecode
• Служебные атрибуты

Важный момент: подписка связывает конкретную запись в базе с инстансом процесса (заданием). В этом ее отличие, скажем, от "Шагов плагина" (Plugin Steps), которые определяют только условия запуска.

Шаг 3. Ожидание. Пока пользователь не выполнил задачу процесс находятся в состоянии "Отложен навечно". В этот момент он находится в состоянии Suspended, с датой PostponeUntil 31.12.9999. В этом состоянии он полностью игнорируется асинхронным сервисом.

Шаг 4. Срабатывание подписки. Предположим, что пользователь изменил запись, на которую был подписан процесс. В нашем случае, пусть он принял задачу в работу (еще не выполнил). При этом веб приложение делает запрос вида:
X++:
exec sp_executesql N'select distinct AsyncOperationId, WaitOnAttributeList from WorkflowWaitSubscriptionBase where EntityId = @entityId and EntityName = @entityName'
Судя по всему, эта проверка выполняется при любом изменении записи, а не только нужных полей, так как заранее не известно сколько процессов ждет чего-то от этой записи.

Шаг 5. Возобновление процесса. Если система понимает, что совпадение найдено - есть процесс, который был подписан на изменение поля statecode, то веб-приложение делает запрос вида:
X++:
exec sp_executesql N'update WorkflowWaitSubscriptionBase
set IsModified = 1, ModifiedOn = @modifiedOn
where EntityId = @entityId
and EntityName = @entityName'
В ту же микросекунду веб-приложением выполняется второй запрос:
X++:
exec sp_executesql N'update AsyncOperationBase 
    set
        StateCode = @readyState,
        StatusCode = @readyStatus,
        ModifiedOn = @modifiedOn,
        ModifiedBy = @modifiedBy
    where
        StateCode = @suspendedState
    and AsyncOperationId in ( <AsyncOperationId> )'
С параметрами @readyState=0,@readyStatus=0. Эти состояния соответствуют запущенному процессу. Данный запрос эквивалентен нажатию на кнопку "Возобновить" на форме задания.

Забегая вперед, скажу, что не встречал в логе запросов, которые выбирали бы записи таблицы WorkflowWaitSubscriptionBase у которых был бы установлен признак IsModified, да и вообще хоть как-то использовали этот признак. Возможно, это какая дополнительная страховка, но если и так, она не помогает в моем случае.

Обращаю внимание на то, что все что происходило в процессе до этого шага - происходило в веб приложении. Асинхронный сервис мог вообще быть отключен и это бы никак не повлияло на работу механизма на данном этапе. Этот сценарий я проверил уже без профайлера. Я отключил единственный асинхронный сервис, обновил состояние записи, после чего снова запустил сервис. Условие выполнилось и процесс пошел дальше.

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

Шаг 6. Что-то страшное. Далее начинает свою работу асинхронный сервис. Процесс интересуется своим состоянием (не знаю, чем WorkflowState отличается от Data) при помощи запроса:
X++:
exec sp_executesql N'select WorkflowState
from AsyncOperationBase
where
    AsyncOperationId = @id'
Далее какой-то запрос вычитывает все поля задачи. Возможно я плохо искал, но я не нашел запроса в нужном временном интервале, который выбирал бы только ее состояние. Видимо это такая оптимизация, чтобы лишний раз потом ничего не вычитывать. Второй вариант: я что-то упустил, так как искал не в изолированной среде, а в производственной, где лог рябит от параллельных процессов на той же сущности.

Теперь, когда у системы есть данные о текущем статусе задачи, она первым делом… удаляет все подписки для текущего процесса!
X++:
exec sp_executesql N'delete WorkflowWaitSubscriptionBase from WorkflowWaitSubscriptionBase with(index(ndx_CascadeRelationship_Asyncoperation_workflowwaitsubscription)) where AsyncOperationId=@workflowId'
После чего ищет подписку (через 17 микросекунд, возможно это погрешность профайлера и она, все же сперва ее ищет, а потом удаляет)
X++:
exec sp_executesql N'select WaitOnAttributeList from WorkflowWaitSubscriptionBase where 
                            EntityName = @entityName and
                            EntityId = @entityId and 
                            AsyncOperationId = @asyncOperationId'
После чего снова создает, на случай если все же удалила:
X++:
exec sp_executesql N'if (select count(*) from WorkflowWaitSubscriptionBase where 
                            EntityName = @entityName and
                            EntityId = @entityId and
                            AsyncOperationId = @asyncOperationId) = 0
                        begin
                            insert into WorkflowWaitSubscriptionBase (WorkflowWaitSubscriptionId, AsyncOperationId, EntityName, EntityId, IsModified, ModifiedOn, Data, WaitOnAttributeList)
                            values (@workflowWaitSubscriptionId, @asyncOperationId, @entityName, @entityId, @isModified, @modifiedOn, @data, @waitOnAttributeList)
                        end'
После чего снова ищет:
X++:
exec sp_executesql N'select WaitOnAttributeList from WorkflowWaitSubscriptionBase where 
                            EntityName = @entityName and
                            EntityId = @entityId and 
                            AsyncOperationId = @asyncOperationId'
Потом снова обновляет подписку, но на этот раз устанавливает WaitOnAttributeList в то же значение "statecode":
X++:
exec sp_executesql N'update WorkflowWaitSubscriptionBase
                        set WaitOnAttributeList = @waitOnAttributeList
                        where EntityId = @entityId and 
                        EntityName = @entityName and 
                        AsyncOperationId = @asyncOperationId'
Шаг 7. Засыпание процесса. Где-то на прошлом этапе процесс понял, что его разбудили зря - пользователь так и не выполнил задачу. Он отработал, эффективно управлял подписками, после чего с чувством выполненного долга засыпает :

X++:
exec sp_executesql N'update AsyncOperationBase 
set 
StateCode = @newState
, StatusCode = @newStatus
, ErrorCode = @errorCode
, Message = @errorMessage
, FriendlyMessage = NULL
, PostponeUntil = @postponeUntil
, ExecutionTimeSpan =  @executionTimeSpan
, ModifiedOn = @modifiedOn
, ModifiedBy = CreatedBy
 where AsyncOperationId = @id
В этот момент он возвращается в состояние ожидания, где находится в состоянии Suspended, с датой возвращения PostponeUntil 31.12.9999.

После чего обновляет свой WorkflowState и, на всякий случай убеждается что заснул:
X++:
exec sp_executesql N'update AsyncOperationBase
set
    PostponeUntil = @postponeUntil,    
    WorkflowState = @workflowState,
    WorkflowIsBlocked = @workflowIsBlocked,
    ModifiedOn = @modifiedOn,
    ModifiedBy = CreatedBy
where
    AsyncOperationId = @id'
Занавес.

Что мне таки и не удалось из всего этого понять, так это почему все-таки виснут эти блядские процессы! Судя по всему, система устроена таким образом начиная с версии CRM 2011. Это первая из известных мне версий, где появилась таблица WorkflowWaitSubscriptionBase. В свое время для нее даже выходил экстренный патч, который позже был включен в UR13: https://support.microsoft.com/en-us/help/2918320. Судя по описанию, это, как раз мой случай, но увы, проблему, похоже, полностью не исправили.

Давайте теперь выстроим некоторые тезисы:
1. Процесс стоит, условие выполнено. Если пнуть его ногой, он поймет, что условие выполнено и пойдет дальше. Следовательно, он не сломался - он просто не обработал изменения, или не был запущен
2. Если судить по запросам, блокировка записи на изменение делается в x.010 (десять тысячных секунды), запись в таблицу аудита - в x.013, а поиск, обновление подписки и запуск процесса - в x.017. Будем считать, что все это происходит в транзакции. Следовательно, процесс обязан был быть запущен
3. У зависших процессов нет модифицированных подписок - только те что по дате обновления совпадают с последним временем запуска процесса. Так как активности процесса должны выполняться в транзакции, обновление флага IsModified должно быть блокировано в конкурирующем обработчике до того момента пока транзакция не будет отпущена
4. Промежуток времени между удалением и повторным созданием подписок - 20 микросекунд. Между изменением записи и запуском самого процесса - 3,5 секунды.

Исходя из этого, гипотеза о конкурирующих изменениях уже не выглядит такой правдоподобной. Если я правильно понимаю, как работают транзакции, тут либо были бы IsModified, подписки, или ошибки, но всего это не происходит. Более правдоподобным кажется некий глюк, который приводит к тому, что процесс вообще не запускается при каких-то условиях, и эта ошибка не отражается в журналах самих процессов. Например, произошла блокировка, или откат транзакции. Увы, мне так и не удалось поймать момент зависания с включенной трассировкой.

Какие уроки можно извлечь из всего этого? Урок первый: не нужно боятся ожидающих процессов. Судя по всему, они совершенно безобидны и не могут вызвать существенных проблем с производительностью: 1-2 дополнительных запроса - это не тот ужас, с полным отказом сервиса которым меня пугали. Урок второй: нужно избегать конкурирующих изменений полей, которые используются в условиях ожидания. Совсем не факт, но, возможно, это поможет избежать зависания процессов.
Размещено в CRM
Просмотров 48153 Комментарии 0
Всего комментариев 0

Комментарии

 


Рейтинг@Mail.ru
Часовой пояс GMT +3, время: 18:53.