Перейти к содержимому

7. Процессы

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

Процессы — основной источник информации о зависимостях в Archlang. Стрелки между модулями на ваших диаграммах выводятся из шагов процессов. Удалите процесс — стрелки, которые он подразумевал, исчезнут. Отдельной операции «нарисовать зависимость» не существует.

process Checkout {
Customer > Orders.CreateOrder
Orders > Inventory.Reserve
Orders > Payments.Authorize
Payments > Ledger.Record
Orders > Notifications.SendEmail
}

Пять шагов; четыре задействованных модуля; четыре выведённые стрелки зависимостей (Customer → Orders, Orders → Inventory, Orders → Payments, Payments → Ledger, Orders → Notifications).

Форма шага

Каждый шаг имеет форму:

Caller > Callee.Interface
  • Caller — модуль (сущность, выполняющая вызов). Слева от >.
  • Callee — интерфейс (обработчик, который запускается). Справа от >.

Стрелка > читается как «вызывает». Является ли нижележащий транспорт синхронным, асинхронным или каким-то ещё, определяется видом интерфейса на принимающей стороне. С базовым видом interface язык трактует каждый вызов единообразно; виды из стандартной библиотеки (command, event, …, Глава 11) добавляют различия sync/async и стилизацию рёбер.

Ошибка, которой стоит избегать. Поместить модуль с правой стороны. Payments > Orders — ошибка разбора; правая сторона должна разрешаться в интерфейс, не в модуль. Валидатор это ловит и сообщает, что там вместо него.

Размещение

Процессы могут жить в трёх местах, все семантически эквивалентные:

Верхний уровень. Отдельно стоящая оркестрация:

process CheckoutFlow {
Customer > Orders.CreateOrder
Orders > Payments.Authorize
}

Внутри тела модуля. Принадлежит этому модулю; полностью квалифицированное имя становится Owner.Process:

module Orders {
process Checkout {
User > Orders.OrdersResource.Add
}
}

Верхний уровень с in. Объявляется плоско, прикрепляется к телу модуля. Зеркалит модульный синтаксис in:

process Fulfillment in Orders {
Orders > Shipping.Schedule
}

Форма in позволяет каждой команде объявлять процессы для модуля другой команды, не редактируя файл этой команды.

Ветвления: if / switch

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

process Checkout {
Customer > Orders.CreateOrder
Orders > Payments.Authorize
if "stripe-customer" {
Payments > Stripe.Charge
} else {
Payments > PayPal.CreateOrder
PayPal > PayPal.CaptureOrder
}
Payments > Ledger.Record
}

if cond Body (else if cond Body)* (else Body)?. Условие — идентификатор или строка в кавычках; тело — блок в фигурных скобках.

process Checkout {
Customer > Orders.CreateOrder
Orders > Payments.Authorize
switch "payment processor" {
stripe {
Payments > Stripe.Charge
}
paypal {
Payments > PayPal.CreateOrder
PayPal > PayPal.CaptureOrder
}
}
Payments > Ledger.Record
}

switch [head] { case-label Body case-label Body ... }. Никакого ключевого слова case — каждый блок начинается со своей метки. Валидатор считает каждый вариант достижимым; граф зависимостей включает каждый интерфейс, упомянутый в любом из вариантов.

Параллельность: parallel

Конкурентные шаги используют parallel:

process OrderFulfillment {
Orders > Shipping.CreateShipment
parallel {
{ Shipping > Notifications.SendEmail }
{ Shipping > Notifications.SendSMS }
}
Shipping > Orders.UpdateOrder
}

Ветки внутри parallel исполняются конкурентно. Каждая ветка — это явный блок в фигурных скобках (или : oneliner для одиночных шагов). Рендерер может разложить их бок о бок, а не последовательно.

Итерация: each

each Item in Collection { ... } итерирует по коллекции элементов. Используется для веерного распространения по множеству неизвестной мощности.

process NotifyAll {
each subscriber in NotificationList {
Notifications > subscriber.SendUpdate
}
}

Пути ошибок: try / catch

try Body (catch [Label] Body)* для явных путей ошибок. Допускается несколько catch; метка у каждого необязательна.

process Checkout {
Customer > Orders.CreateOrder
try {
Orders > Payments.Authorize
} catch "declined" {
Orders > Notifications.SendEmail
}
}

Подпроцессы

Повторяющиеся последовательности шагов становятся подпроцессами — переиспользуемыми помощниками, вызываемыми через do:

subprocess RecordEvent(name) {
Orders > Ledger.Record
}
process Checkout {
Customer > Orders.CreateOrder
do RecordEvent("checkout_started")
Orders > Payments.Authorize
do RecordEvent("payment_authorized")
}

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

Порядок поиска при выполнении do X(...):

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

Стабильные идентификаторы

Процессы несут стабильные идентификаторы так же, как и модули:

process #flow42 Checkout {
Customer > Orders.CreateOrder
}

Форматтер выдаёт #flow42 при сохранении. Идентификатор позволяет диффам распознавать переименованный процесс. См. Главу 13.

Валидация

Парсер и валидатор обеспечивают инварианты процессов:

  • Вызывающий (слева от >) должен разрешаться в модуль (включая stdlib-модули actor после импорта стандартной библиотеки — см. Главу 11).
  • Вызываемый (справа от >) должен разрешаться в листовой интерфейс (не в фасет, не в модуль).
  • subscribes:, указанный на обработчике, должен указывать на существующее событие.
  • Вызов do X должен разрешаться по порядку поиска подпроцессов.

Сбои появляются как диагностики LSP в вашем редакторе и как ошибки с кодом 1 от archlang validate.

Что вам дают процессы

Когда у вас есть процессы, автоматически следует несколько вещей:

  • Граф вызовов сервисов. Модули, связанные рёбрами Caller > Callee.Interface по всем вашим процессам.
  • Критические пути. Поток запроса в десять шагов виден с одного взгляда.
  • Радиус поражения. Когда один сервис падает, какие процессы ломаются? Тривиально выводится.
  • Влияние изменений. Когда вы удаляете интерфейс, валидатор сообщает вам про каждый шаг процесса, который от него зависел.

Ничто из этого не требует поддерживать отдельный каталог зависимостей. Каталог — это объединение каждого шага процесса в рабочем пространстве.

Итоги

  • Процесс — последовательность шагов Caller > Callee.Interface.
  • Вызывающие — модули или акторы; вызываемые — листовые интерфейсы.
  • Ветвления (if / switch), параллельность (parallel), итерация (each) и пути ошибок (try / catch) — первоклассные.
  • Подпроцессы — переиспользуемые помощники, вызываемые через do.
  • Процессы могут жить на верхнем уровне, внутри тела модуля или присоединяться через in.
  • Граф зависимостей выводится из процессов; стрелки отдельно не рисуются.

Что дальше

Глава 8: Проекции → — кураторские проекции модели для конкретных аудиторий.