О плохом и хорошем коде для чайников, часть 3. Загадка о невыполнившемся коде.

29 2

Начало в О плохом и хорошем коде для чайников. Часть 2.

Предисловие

- Но чёрт возьми, Холмс! Как?

Программист поддержки угрюмо смотрел в монитор. Вот уже два часа он искал ошибку в сценарии, приведшую к тому, что часть пользователей не получила свои регулярные уведомления. Сценарий был донельзя простым: найти записи справочников, проверить их срок действия, для истекших изменить реквизиты и отправить уведомления пользователям. И несколько дней назад эта простота дала сбой: записи изменились, но об этом никто не узнал. Очевидно, подумал программист, произошла какая-то некритичная ошибка, даже не зафиксированная в логах, но прервавшая выполнение вычислений. Как же её отловить?

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

Всё, что вы хотели знать об ошибках, но боялись спросить

Не ошибается лишь тот, кто ничего не делает.

Народная мудрость

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

Обработка исключений

...и это и есть его основная ошибка.

Редко упоминаемое окончание народной мудрости

Для обработки возникающих исключений в ISBL есть целых две конструкции: 

  try
    <блок вычислений, в котором может возникнуть исключение>
  except
    <блок вычислений, выполняемый при возникновении исключений в блоке try>
  endexcept

и

  try
    <блок вычислений, в котором может возникнуть исключение>
  finally
    <блок вычислений, выполняемый вне зависимости от возникновения исключений в блоке try>
  endfinally

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

  1. Код в блоке finally, в отличие от except, отрабатывает всегда.
  2. При использовании except исключение, возникшее в блоке try, подавляется, что даёт программисту возможность самостоятельно его обработать: записать в лог, откатить изменения в объектах и тому подобное.
  3. При использовании finally исключение, возникшее в блоке try, генерируется после завершения работы кода в блоке finally, если не было подавлено.

С учётом этих принципов можно выделить разницу в применении except и finally: try-except используется для кода, ошибки в котором нужно перехватить, а try-finally – для кода, ошибки в котором не должны повлиять на выполнение определённых действий. Иногда конструкции используются совместно, вложенными друг в друга следующим образом:

  try
    …
    try
      …
    except
      …
    endexcept
    …
  finally
    …
  endfinally

Небольшой пример работы с этими конструкциями:

  try
    XMLDocument = CreateObject('MSXml.DomDocument')
    XMLDocument.Load(SourceFolder & '\' & XMLName)
  except              
    XMLErrorText = LoadStringFmt('DIR8F58DB00_0628_4120_974A_EE46970F0780'; 'COMMON'; ArrayOf(XMLName; GetLastException().Message))
    WriteFile(LogFileName; 'Y'; Now() & XMLErrorText  & CR)
    MoveFile(SourceFolder & '\' & XMLName; ProcessedWithErrorsFilesFolder & '\' & XMLName)
    XMLDocument = nil
  endexcept

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

Ещё один пример:

  try
    Reference.Events.DisableAll   
    Reference.Open
    RecCount = Reference.RecordCount
    if RecCount <> 0                    
      foreach Record in Reference 
        DoSomething(Record)
      endforeach  
    else
      WriteFile(LogFilePath; 'Д'; Now() & ' Справочник пуст.' & CR)
    endif
  finally
    Reference.Close   
    Reference.Events.EnableAll
  endfinally

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

Генерация ошибок

- Ещё раз напишешь слово «напишешь»
без мягкого знака, я приеду и выпорю!
- Напишеш напишеш напишеш

Грустная быль времён HTML 1.0

Помимо обработки ошибок ISBL также предоставляет возможность их намеренной генерации, причём иногда это является единственным легальным способом прервать ход вычислений. Сгенерировать ошибку (исключение, IException) позволяют две функции ISBL: CreateException(Name, Message, Cathegory) и Raise(Exception). Первая, как нетрудно догадаться по её имени, создаёт исключение, вторая поднимает его.

Небольшой пример для понимания того, как это работает:

  //Событие «После запроса параметров» блока ТМ «Проверка данных договора и подписывающих»
  Params = Object.WorkflowParams
  RefKind = IfThen(Assigned(Params.ValueByName('Contract').Value); 'Main'; 'Other')
  RefInfo = IfThen(RefKind == 'Main'; Params.ValueByName('Contract').Value; Params.ValueByName('Appendix').Value)
  Reference = RefInfo.Reference
  Reference.Open()
  Reference.OpenRecord()
  SigningUser = GetSigningUserForContract(Reference)
  Reference.CloseRecord()
  Reference.Close()
  Reference = nil
  if SigningUser <<>> ''
    Params.ValueByName('SigningUser').Value = SigningUser
  else
    Raise(CreateException('SigningUserNotDefined'; LoadString('DIRECCD8759_6CAE_403C_9FFD_B6964356198C'; 'COMMON'); ecException))
  endif

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

Другой пример намеренной генерации исключений:

  ApprovalGroup = ServiceFactory.GetGroupByCode(ApprovalGroupCode)
  Approvers = ServiceFactory.GetGroupMembers(ApprovalGroup)
  if Approvers.Count < 1
    Raise(CreateException('Ошибка'; 'Группа согласования ' & ApprovalGroup.Name & ' пуста. Разместите запрос на добавление ответственного в группу согласования.'; ecWarning))
  endif

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

Подобным образом (генерация исключения по условию) работает ISBL-функция Assert. Она принимает на вход логическое выражение (то самое условие) и сообщение, которое отобразится пользователю в случае, если условие не выполняется. Как и Raise, эта функция прерывает текущий блок вычислений, если её не обернуть в try-except. Фактически в обоих приведённых выше примерах следовало бы использовать её, но допустимыми являются оба варианта, и использование какого-то одного из них – в большей степени вопрос привычки, чем корректности кода.

Ещё один способ сгенерировать исключение в ISBL-коде – это использовать функцию Exit() с передачей в неё какого-нибудь текста в качестве параметра. Текущий блок вычислений при этом будет завершён, а в лог запишется исключение типа ESBUserException с сообщением, переданным в параметре.

Логирование

Полковнику Администратору никто не пишет

Почти Би-2

Лучшее, что можно сделать с ошибкой, вне зависимости от способа её получения – это записать её в лог. Если мы имеем дело с объектом-исключением, проще всего воспользоваться его методом WriteToLog:

  Exception = GetLastException()
  Exception.WriteToLog()

WriteToLog запишет все доступные данные об исключении в один из стандартных логов DIRECTUM (подробнее о логах и работе с ними см. здесь и здесь), в зависимости от того, где выполнялись вычисления. Но можно записывать и собственные логи, что позволяет сделать ISBL-функция WriteFile:

  Exception = GetLastException()
  WriteFile(LogFilePath; ‘Y’; ‘Что-то пошло не так. Подробнее: ’ & Exception.Message & CR)

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

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

 

Вложенные вычисления

Открыть сумочку, расстегнуть карманчик,
взять кошелёчек, застегнуть карманчик,
закрыть сумочку…

Детский анекдот

Всё, сказанное выше, может относиться как к одиночным блокам вычислений, так и к целым сценариям. Бывает так, что сценарий на 600 строк полностью «завёрнут» в один огромный try, а блок except абсолютно пуст. Бывает, что в вычислениях ТМ вызывается сценарий, в сценарии идёт обращение к функции, а уже функция генерирует исключение. В результате поиск места возникновения ошибки превращается в увлекательный квест «Пойми, на какой строке сломались вычисления», потому что обработка исключений при всех её достоинствах обладает и недостатками. Один из них заключается в том, что при срабатывании Raise в блоке except в лог запишется не номер строки, на которой прервался try, а номер строки, содержащей Raise. Поэтому рекомендации по генерации и обработке ошибок сводятся к двум простым правилам:

  1. Выстраивать блоки try-except необходимо таким образом, чтобы внутри одного try было не больше двух точек потенциального возникновения исключений (а в идеале – одной);
  2. При использовании Raise в блоке except лучше поднимать не самостоятельно сгенерированное исключение, а системное, полученное при помощи функции GetLastException, потому что для ликвидации ошибки необходимо видеть её настоящий текст, который позволяет установить место её возникновения.

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

Вместо заключения

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

Спасибо за статью! Очень познавательно и доступно.

Анастасия, спасибо  рада, что получилось понятно описать всё это)

Авторизуйтесь, чтобы написать комментарий