понедельник, 5 августа 2013 г.

Как я исправил невоспроизводимую ошибку SharePoint с помощью UX-паттерна

В прошлый раз я теоретизировал о необходимости знаний по UX у SharePoint разработчиков. Сегодня хочется рассказать об интересном прецеденте использования UX-паттерна для исправления невоспроизводимой ошибки SharePoint.

Дано

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

Иногда один или несколько сайтов не создаются по неизвестной причине. Ошибка валится на Webs.Add и она плавающая: то есть, то нет. Более того, по закону подлости она возникает исключительно на production environment. И это еще не всё, когда ко мне попала эта задача, логи с последней ошибкой уже потерлись, поэтому я даже не располагал сообщением об ошибке!...

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

Стандартный подход

Стандартным подходом было бы поочередное исследование кода решения и затем кода Webs.Add через рефлектор, и попытка понять в чем может быть дело (скорее всего неудачная, т.к. Webs.Add довольно быстро уходит в Unmanaged код).

Можно конечно пробовать разные варианты параметров или порядка или какие-нибудь задержки... Но поскольку ошибка не воспроизводится нигде кроме production, а production можно обновлять только раз в 1-2 месяца, да еще и используется этот модуль не интенсивно... В общем методом "тыка" можно было бы растянуть проблему на годы. Собственно, это и происходило - когда я взялся за проблему, она существовала уже больше года.

UX подход

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

В моих любимых Apple UX Guidelines есть следующий пункт:

Display an informative, actionable alert message when something goes wrong. An alert should clearly convey what happened, why it happened, and the options for proceeding. Describe a workaround if one is available and do whatever you can to prevent the user from losing any data. Avoid using an alert to deliver information that the user can’t act upon.

В этих четырех предложениях - буквально всё, что вам нужно знать об обработке ошибок! На всякий случай по-русски:

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


Что это означает в нашем случае?
  1. Нельзя "глотать" ошибки создания сайтов.
  2. При отображении ошибки, необходимо предоставить не только информацию о том, что случилось и почему, но также необходимо предоставить возможность действия.
Решение

Решение очень простое, но при этом надежное и удобное.

Во-первых, я добавил информацию об ошибке: в случае, если один или несколько сайтов не были созданы, отображается таблица со статусом по каждому из сайтов (Success/Failure).

Во-вторых, рядом с Failure я добавил кнопку Retry. ВСЁ!


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

Такие улучшения интерфейса безусловно повлекли за собой некоторое количество работы и немного рефакторинга, т.к. для того, чтобы сделать возможность Retry, пришлось отделить код создания сайтов в обособленный метод, но в любом случае вся работа заняла 2 дня - с большими перерывами на чай и бильярд. Стоит ли говорить, что investigation мог бы занять практически сколько угодно времени и ни к чему не привести?

Отдельно отмечу: это решение полностью закрывает задачу. ПОЛНОСТЬЮ. С точки зрения пользователя, проблемы больше нет. Нажать пару раз кнопку Retry, при том что интерфейс используется пару раз в месяц - да это вообще не вопрос, верно же?

Выводы

  1. Ошибку можно исправить, не исправляя ее
  2. Я уже писал, что знания по UX меняют сам подход к разработке. И к исправлению ошибок - тоже. Всё, что мы с вами видели выше - это просто подход к решению с другой стороны. Много раз я убеждался, что это реально работает и дает плоды.
Так что вот. И да пребудет с вами UX! :)

21 комментарий:

  1. Андрей, крайне полезная и показательная статья. Спасибо!

    ОтветитьУдалить
  2. 1) А в чем все таки причина ошибок? Может тупо можно было код 3 раза повторить с перерывами в случае ошибки?
    2) Я так понял, что основная проблема, которая мешала жить - try с пустым catch. Возможно в самом интерфейсе и не было проблем, просто не сделали отображение ошибок.
    3) Крайне сомнительно выглядит функция генерации сайтов для end-user. Возможно это админская функциональность, тогда почему не powershell? В нем можно при ошибке сразу из логов собрать нужные строки и подготовить пакет.

    ОтветитьУдалить
    Ответы
    1. В try catch ошибка логировалась, просто т.к. функциональность использовалась очень редко, последние логи когда была эта ошибка уже потерлись. И эта ошибка могла еще несколько недель не возникать - там как повезет, говорю ж она плавающая.

      Возможно 3 раза повторить с перерывами и помогло бы. А если нет? А какой делать перерыв? А что если истечет таймаут запроса - выносить в джоб? Это по-моему намного больше геморроя чем с кнопкой Retry.

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

      Удалить
    2. даже "просто логирование" в try\catch - повод настучать по печени тому, кто писал код. Любая ошибка, не позволяющая выполнить работу полностью, должна быть отображена пользователю.

      Судя по описанию - отличный кандидат для workflow. Причем можно прямо в sharepoint designer фигачить, достаточно пару кастом-экшенов написать.

      Если бы сразу было сделано на WF, то:
      1) На было бы проблем с индетификацией ошибок, все светилось бы в History List.
      2) Можно было обойтись стандартными интерфейсами (это бы пипец как удешевило решение).
      3) Добавить возможность retry было был легко, как 5 копеек - поменять custom action.
      4) Да еще и интерфейс был бы поприятне, так как создание 10 сайтов работает медленно, пользователю приходится ждать. А долго ждать никто не любит.

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

      Удалить
    3. Возможно ты прав насчет того, что WF является более подходящим форматом, хотя там довольно сложная логика, поэтому пришлось бы ту же самую логику паковать в wf custom actions. Т.е. фактические затраты на создание модуля врядли сильно уменьшились бы. Пишешь тот же самый код, создаешь те же самые поля настройки, просто делаешь это в виде WF вместо app page.

      Проблема в том, что если ты не готов работать с существующими решениями и у тебя подход "тут всё неправильно изначально сделано, надо переделывать, я ничего не буду исправлять, переделать быстрее", то это на самом деле очень рисковый подход и обычно он заканчивается гораздо более серьезными трудозатратами, чем 2 дня. Я бы оценил переход на WF для конкретно этого проекта в неделю-две. В существующем решении как правило есть куча костылей, которые при сложном рефакторинге навроде перевода решения в WF теряются. Потом начинается многодневное разгребание этих "новых старых" ошибок. Потом еще есть вероятность упереться в какую-нибудь стенку в WF, ну например форма инициации WF получилась бы довольно сложной, т.к. надо указать количество сайтов и для каждого сайта потом настройки - и плюс там есть другие хитрые контролы на этой форме. Всё это могло вылиться в довольно сложную форму, а на инфопасе сложную форму наваять - это очень рисковое предприятие.

      На мой взгляд, бессмысленно разбираться в том "какой идиот это сделал" и "почему это сразу не сделать было правильно". Есть конкретная проблема, есть конкретная задача ее решить. Ты точно уверен что ты бы предложил ПЕРЕПИСАТЬ решение в данной ситуации? Обвинив еще вдобавок одного из своих коллег по компании и поставив его в тень, помимо всех остальных рисков при переделке?

      Удалить
    4. Не ту же самую логику, а только часть, которая стандартными action_ами не покрывается. На проверку оказывается не более 2/3 кода, переезжает в actions, а чаще всего гораздо меньше.

      Форма инициации в WF не нужна. Нужно делать WF для элемента списка. Потом на список можно навесить права, утверждения и прочие радости.

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

      Я бы предложил до того как код написан - сделать design review. Пусть человек просто расскажет что он собирается делать для решения задачи. Обычно это занимает 20 минут и выявляет много проблем. Далее после написания кода сделать code review, чтобы исключить те же самые проблемы с проглатыванием ошибок.

      И вот выяснится что на продакшене падает, потому что сайт не создается. Останется дописать код retry в кастом экшены проблема с высокой долей вероятности будет решена (а не просто вывалена пользователю).

      То что ты пишешь - не является по сути решением, так как проблема не уходит, а только не "проглатывается" и к UX отношения не имеет. Root cause не выяснен. Способ избегания проблм в будущем не предложен.

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

      Удалить
    5. Как я уже писал выше в статье, речь идет о создании конфигурируемого количества сайтов, причем для каждого сайта задается его url, title и тип. В зависимости от типа сайта используются разные шаблоны сайтов и по разному производится донастройка этих сайтов. Т.к. речь идет о SP2010, что я думаю было понятно из скриншота, циклы - это уже проблематично для WF. Перетаскивание параметров из формы инициации в WF для каждого элемента цикла - еще одна проблема. Т.о. я подозреваю что в данном случае придется в какой-нибудь "спец" initiation field паковать тупо все данные, передавать всё это в 1 wf custom action, который всё это будет доставать из спецфилда, распарзивать и уже организовывать цикл. Т.к. цикл будет внутри WF custom action, то весь остальной код тоже довольно логично будет запихнуть внутрь этого же custom action.

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

      То что я пишу, как раз является полностью достаточным решением по причинам, уже озвученным в статье и в комментариях здесь:
      1. Кнопка Retry дает гораздо больше шансов на исправление ошибки, чем автоматический Retry;
      2. Подход с кнопкой Retry дает пользователю больше информации, больше контроля над ситуацией и службе поддержки - больше шансов на выяснение причины ошибки, в случае если Retry не поможет.
      3. Наконец, в "вываливании" ошибки пользователю в данном случае нет ничего плохого, т.к. интерфейс используется несколько раз в месяц, а ошибка возникает и того реже, и нет проблемы в том, чтобы понажимать кнопку Retry если это приходится делать редко. В данном конкретном случае "вываливание" ошибки не является жутко плохим актом черных сил. Вреда пользователю не нанесено, помощь оказана, контроль над ситуацией предоставлен. Что не так?

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

      Есть класс ошибок, которые воспроизводятся (и причем даже не всегда) только на production - к которому доступ сильно ограничен, а код туда можно выгружать только 1-2 раза в месяц. Root cause ошибки в этом случае выяснить очень сложно и затратно по времени. Идея статьи в том, что иногда можно обойтись без этого и избежать огромных потерь времени, связанных с поиском ошибки, бесконечным анализом кода, многократной пробой "методом тыка" и т.п.

      Удалить
    6. Без понимания причин ошибки любое действие - это угадывание. Давая кнопку пользователю - ты просто перенес на него отвественность. Если ошибка действительно плавающая, то добавление кнопки retry, ничем не лучше автоматического переоткрытия сайта, веба и всего остального. Если для возникновения ошибки существуют вполне контролируемые пользователем предусловия, то надо проверять предусловия, а не делать кнопку retry. Кстати в этом случае retry может не помочь вообще (тупо существует сайт с таким урлом).

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

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


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

      Удалить
    7. "Существует сайт с таким же урлом" - не будет такой ошибки, т.к. создается сначала сайт проекта, потом внутри него создается еще куча сайтов. Ошибка возникает как раз на внутренних сайтах.

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

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

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

      Удалить
  3. Думаю, причина ошибки в concurrency. Может быть клиентов и удовлетворил вывод сообщение об ошибке, но всё-таки ошибка-то осталась неисправленной. Надо было в коде написать перехват исключения и повторную попытку создания узла, вот это было бы исправление.

    ОтветитьУдалить
    Ответы
    1. Возможно повторное создание помогло бы, а возможно - нет. Не угадаешь, сразу ли надо пересоздавать, или надо таймаут перед пересозданием, или надо обновить SPContext или еще что-нибудь... Может надо передиспозить родительский веб, откуда ты знаешь заранее?

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

      Способ с кнопкой Retry дает пользователю больше информации, больше контроля над ситуацией, больше шанса что пересоздание поможет (потому что в случае с кнопкой Retry у нас новый SPContext, заново взят родительский веб и т.п.), и этот способ гораздо более правильный с точки зрения UX: что-то пошло не так? мы сообщили об этом и предоставили возможность действия. Т.е. к примеру пользователь может позвонить сразу в поддержку после пары попыток пересоздания и тогда можно в реал-тайм смотреть логи и пробовать разные варианты под разными пользователями и т.д.

      Удалить
    2. >> Не угадаешь, сразу ли надо пересоздавать, или надо таймаут перед пересозданием, или надо обновить SPContext или еще что-нибудь...

      А чего гадать? Взять и сделать всё сразу: добавить таймаут, обновить объекты и пр. Ну если после 3-х попыток не получилось, то да, выводить клиенту информативное сообщение "произошла неожиданная ошибка".

      >> Способ с кнопкой Retry дает пользователю больше информации, больше контроля над ситуацией

      Он даёт пользователю сообщение типа "something went wrong" и убеждение, что программисты не знают, что делают. Типа как совет "перезагрузить компьютер" или "а вы пробовали выйти и снова зайти".

      Удалить
    3. Непонятно, как выбрать величину таймаута. Если сделать слишком большой - скорее всего это приведет к необходимости выноса кода куда-то в таймер джоб, если задержку сделать слишком маленькой может не помочь. Чистое угадывание. А SPContext вообще так просто не пересоздашь. Плюс к тому, код для пересоздания объектов - это новый код, любой новый код это вероятность ошибки. И ты не сможешь прям вот так вот взять и протестировать этот новый код: выгрузка на продакшн происходит в лучшем случае 1 раз в месяц, и интерфейс используется редко, и ошибка возникает еще реже. Сценарии где что-то может не сработать в таких ситуациях нужно исключать.

      Вывод кнопки Retry сразу дает минимум 10-20 секунд задержки, пока страница загрузится и пользователь прочитает что случилось. Также этот способ автоматически, без дополнительного кода, дает все объекты пересозданными, включая SPContext и низлежащий SPRequest, потому что это новый запрос.

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

      Удалить
    4. А зачем пересоздавать SPContext? Можно же SPSite новый взять, больше ничего не надо, я думаю.

      Удалить
    5. С шарепойнтом никогда не угадаешь, когда можно просто SPSite новый взять, а когда нужно проделать другие танцы с бубном. Может вообще ничего пересоздавать не надо, я хз, я не пробовал. Потому что пробовать в этой ситуации - это означает 1.5 месяца ждать результатов попытки.

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

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

      Как-то так :)

      Удалить
  4. Кстати текст отображаемой ошибки доставляет неимоверно :)

    >> Сообщение должно четко описывать что случилось, почему это случилось, и что делать дальше.

    ОтветитьУдалить
    Ответы
    1. Если ты про "Something unexpected just happened", то это я просто для тестирования делал if (random.Next(2) == 1) throw new Exception("Something unexpected just happened");, т.к. настоящая ошибка не воспроизводится на тестовом окружении.

      Удалить
    2. На продакшене скорее всего будет "Unexpected error, call administrator." или что-то в этом роде :)

      Удалить
  5. С одной стороны решение элегантное, с другой, что будет если человек нажмет 10 раз кнопку Retry и ничего не случится? меня бы взбесило.. :)

    ОтветитьУдалить
    Ответы
    1. На скриншоте в статье можно заметить наличие дополнительного текста вверху, который описывает, что надо делать в этом случае (обратиться в тех поддержку и передать им текст из колонки error details).

      Удалить

Внимание! Реклама и прочий спам будут беспощадно удаляться.