Skip to content

Instantly share code, notes, and snippets.

@avesus
Last active August 29, 2015 14:22
Show Gist options
  • Save avesus/39fdf48d4f88c6dbafb2 to your computer and use it in GitHub Desktop.
Save avesus/39fdf48d4f88c6dbafb2 to your computer and use it in GitHub Desktop.
Errors Handling & Code Organisation
  • Use http://sweetjs.org/ to implement condition checkers. Монада maybe на Promise'ах (строчка return default;):
newAsyncFunc().then(function(success) {
    return anotherAsync();
  }, function(error) {
    return default;
  }).then(function(success) {
    return yetAnotherAsync();
  }, function(error) {
    return default2;
  });
  • Bugs: all kinds of invalid implementation. Detected as external contract violation ONLY. All kinds of internal assertion checks ONLY helps to localize a bug. Detection technics:

    • overall integration testing to check contracts

    • runtime checks (???) - localization technique only, we couldn't dynamically predict which result should be (for a RANDOM client request, only CLIENT knows which result is correct - but ever this is not true. We could respond with correctly-looked result, but it could be erroneous - and nobody will know while some output of a subsequent system (or a user) will got outcome). When such bug will be localized and fixed, there will be a regression check (runtime check or in tests only?), could the bug be detected by means of post-assertions?

      Runtime check could be implemented in system code after it does its job - right before return result to client. Pre-processing checks are for client contract check (see Valid input below) and invariants (system's internal state and depended systems conditions, i.e., network, HDD/RAM size, CPU load, etc.) System could check available RAM and CPU before function execution if request should be processed quickly. Other ways to guarantee invariants - reserve RAM/CPU/HDD/Network Bandwidth and another resources before processing.

  • Invalid input: Intentional or unintentional contract violation from client side. Should be detected by external layer of system's input. Graceful result code handling only - exceptions shouldn't be passed between system and client, because it will be inverted control of client by system. Result code handling should be a part of system's contract - client's failure to specify a valid input should not move system to its contract invalidation.

  • System errors: Contract violations by external executive actors which are NOT PART of the system.

    • MUST be handled gracefully
    • MUST be retryable
    • MUST be passed to client level to control

    Contract violations by internal parts of the system (any part, in principle): System could decide to retry with another (not buggy now) part at runtime (but consider to log bug!). Such retry is considered very rare, so... If retry is not possible, the part's violation is violation of the whole's contract - the system contains bugs, and have to report it loudly (fail fast?). In case of such fails, system should switch to fail-safe mode with minimum basic functionality. In principle, production-grade system, when it provided NEW functionality to a client, couldn't switch back to previous version - but client could choose to switch back. If system was worked with user's data (wich HAVE TO be migrated backwards to previous version), it should save user's data as much as possible (but new functionality will be lost!). Described failover switch to previous "stable" version could be used at runtime for a user who triggered some buggy parts. But every another user could too. So, beta-testing could come to continuation of using buggy parts - if it is possible. Downgrade is always a user-choosen step. (In most systems it ever not supported at all). So, if not downgrade, then what? Just store user's data. In new version. Show excuses.

    Contract violations by executive actors which are called by system to do its job. Must be handled gracefully in all places. ONE IMPORTANT NOTE ABOUT CONTAINERS AND OWNERSHIP: If executive actors are available to client (semantically known and accessible), system errors handling COULD be raised to client's level by specifying system error condition in system's contract. Otherwise, errors in internal parts of the system which COULD be handled, MUST be handled. BUT: errors are: a) detectable contract violations and b) bugs.

@avesus
Copy link
Author

avesus commented Jun 9, 2015

  1. Нашёл баг (нереализованная функциональность - тоже баг),
    ДО того, как его исправить,
  2. Напиши интеграционный "тест", который должен только ПОДЫГРЫВАТЬ СИТУАЦИЮ, в которой БАГ воспроизводится. Это входные данные и состояние системы. Если в искусственном окружении трудно подыграть состояние и входные данные, просто перейди к п.3.
  3. Напиши post-assertion (ВНУТРИ САМОЙ СИСТЕМЫ), который должен СРАБАТЫВАТЬ,
    а интеграционный тест из п.2 - падать (если он написан).
    Срабатывание его, разумеется, на той ситуации (входных данных от клиента и инварианте
    внутреннего состояния), на которой баг ВОСПРОИЗВОДИТСЯ.
  4. Исправь баг (или реализуй функциональность) - post-assertion должен ПЕРЕСТАТЬ СРАБАТЫВАТЬ (для этого нужно запустить тест или выкатить бету).

Замечание: при неправильных входных данных валидация клиентского контракта может быть сделана до (хотя и не обязательно) выполнения каких-либо действий системой.

Если система не работает на неправильных входных данных, а как-то ДОЛЖНА, то это тоже баг (отсутствие функциональности). Необходимы всё те же шаги:
2. Интеграционный тест, если есть возможность.
3. Post-assertion, который срабатывает на неправильных входных данных с отсутствующей проверкой. Условие здесь творческое: к каким именно ПОСЛЕДСТВИЯМ приводит отсутствие ВАЛИДАЦИИ входных данных?
4. Добавлена обработка неправильных входных данных (проверка того, что клиент выполнил его контракт). Т.к. теперь валидация контракта клиента вошла в контракт системы, то она должна gracefully возвращать коды ошибок, а не бросать exception'ы!!!

В итоге, код будет выглядеть, как:

function foo(input) {
  // Check client's contract
  if (!valid(input)) {
    return false;
  }

  // Check system's state
  if (!valid(systemState)) {
    return false;
  }

  if (!valid(availableResources)) {
    return false;
  }

  var result = doWork();

  if (!valid(result)) { // could aggregate statistical info in time, for, ex. rnd() function.
    throw InvalidResultAssertion;
  }

  return result;
}

@avesus
Copy link
Author

avesus commented Jun 9, 2015

Первая часть кода из предыдущего примера возвращает false (или каким-то иным ДОКУМЕНТИРОВАННЫМ способом описывает, что не так с клиентом / системой / моментом во времени). Последняя часть - проверяет код в doWork() на безбажность. Т.к. здесь 3 РАЗНЫХ типа "результатов", то явно, без монад проверки ошибок не обойтись. То есть, возвращаемое значение из foo() может работать прямо, или разворачиваться fail.

@avesus
Copy link
Author

avesus commented Jun 9, 2015

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

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

Клиентский код тоже должен делать какие-то задачи. К клиентскому коду предъявляются алгоритмические требования: не "что", а "как". Клиентский код должен иметь корректную форму.

@avesus
Copy link
Author

avesus commented Jun 9, 2015

Клиентский код тоже может быть тестируем:

  1. Он должен иметь правильную форму - корректный алгоритм;
  2. Он должен выполнять все клиентский контракты вызываемого серверного кода;
  3. Он должен правильно принимать и интерпретировать результаты (контракт приёма результатов).

@avesus
Copy link
Author

avesus commented Jun 9, 2015

В event-driven архитектуре результат работы серверного кода передаётся посредством вызова клиентского метода. Этот метод уже должен иметь корректную форму (ура, инвариант возврата результата можно проверить!), может проверять "входные" аргументы (хотя тут совсем не обязан!).

Тут любопытная симметрия обнаруживается: вызов метода = возврату результата. Поэтому, можно попробовать в клиентском коде, в части формирования вызова, добавить "post-assertion" проверки на безбажность результатов работы клиентского кода ДО этого вызова, своего рода тест клиентского пре-контракта.

@avesus
Copy link
Author

avesus commented Jun 9, 2015

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

@avesus
Copy link
Author

avesus commented Jun 9, 2015

Таким образом, симметричный результат можно привести к вызову любого метода: вызов метода должен подставлять данные, которые идут на вход метода, в реактивную формулу-триггер (вместе с другими данными, группируемыми в формуле по любому критерию, например id сессии/запроса и т.п.). При срабатывании триггера, должен выполняться код обработки (вызов внутреннего, приватного, метода). Без динамического процессинга, любой публичный метод лишь ставит данные на его входе в идентифицируемую транзакцию триггера (присваивает значение внутренней переменной) и вызывает пересчёт реактивной функции. Если результат вычисления положительный, должен выполняться клиентский код, "употребляющий" результат - аггрегат данных транзакции.

@avesus
Copy link
Author

avesus commented Jun 9, 2015

Таким образом, фрагменты клиентского кода, имеющие дело с результатами вызова серверного кода, могут быть представлены в виде методов, реагирующих на аггрегирующий реактивный триггер. Так как код имеет структуру и место, то вызов каждого метода можно представить в контексте его позиции в структуре клиентского "алгоритма", таким образом повышая code reuse и composability. Важно уметь правильно передавать информацию о контексте - положении вызываемого в данный момент метода в алгоритме (порядок, номер итерации и т.п.). Суть в том, что срабатывание триггера реализует понятие call stack'а. Контекст реализует понятие instruction pointer'а.

@avesus
Copy link
Author

avesus commented Jun 9, 2015

Соответственно, триггеры при срабатывании должны иметь не только метод-обработчик ("строчку кода", аналог ассемблерных инструкций), но и положение этой строчки в контексте остального кода (других методов), некий объект closure, информацию о положении этой строчки в алгоритме.
Самое важное, что здесь нужно понять - сам алгоритм, в котором вызывается метод. От этого зависит СЛЕДУЮЩИЙ шаг. То есть, после обработки триггера вызовом приватного метода, для текущей транзакции должен быть определён набор следующих триггеров - должно измениться состояние триггеров текущей транзакции. Иначе следующий вызов такого же метода приведёт к активации того же самого триггера, который, в свою очередь, опять вызовет текущий приватный метод.

@avesus
Copy link
Author

avesus commented Jun 9, 2015

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

@avesus
Copy link
Author

avesus commented Jun 9, 2015

Promise'ы делают именно это - позволяют описывать высокоуровневый алгоритм на низкоуровневых методах.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment