OxL Справочник OxL
РусскийEnglish

Шаг 12. Маленький сервис

Зачем#

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

  • service — контракт: список rpc-методов, их параметры, результаты и ошибки;
  • impl — реализация контракта;
  • actor — состояние; sharded by — по экземпляру на ключ, распределённый по узлам;
  • cluster — какие роли какие сервисы обслуживают и какие акторы размещают.

Вызов Inventory.reserve("pen", 2) выглядит одинаково, живёт ли реализация в этом же процессе или на другом узле. Поэтому сервис можно сначала написать и протестировать в одном процессе, а потом разнести по кластеру, не меняя вызывающий код.

Пример#

Склад: резервирование товара. Состояние (сколько осталось) хранит шардированный актор, по одному на артикул; сервис — вход снаружи.

oxl✓ проверяется в CI
@error data OutOfStock { sku: str; left: i32 }

data Item { sku: str; qty: i32 }

actor Shelf sharded by sku: str {  // по одному актору на артикул; состояние живёт в акторе
  var left: i32 = 5
  msg take(n: i32) -> Item | OutOfStock {
    if left < n { fail OutOfStock{ sku: sku, left: left } }
    left -= n
    Item{ sku: sku, qty: n }
  }
  msg count() -> i32 { left }
}

service Inventory {  // контракт: что видно снаружи
  @public rpc reserve(sku: str, n: i32) -> Item | OutOfStock
  @public rpc stock(sku: str) -> i32
}

impl Inventory {
  rpc reserve(sku, n) { Shelf(sku).take(n)? }  // сервис делегирует актору
  rpc stock(sku) { Shelf(sku).count()? }
}

cluster shop {
  role all { serves Inventory; hosts Shelf }  // узел обслуживает сервис и размещает акторы
}

fn main() io, net {
  for n in [2, 2, 2] {
    match Inventory.reserve("pen", n) {
      Item i => print("reserved {i.qty} {i.sku}")
      OutOfStock e => print("out of stock: {e.left} {e.sku} left")
      NetError => print("network")
    }
  }
  match Inventory.stock("pen") {
    i32 n => print("left {n}")
    NetError => print("network")
  }
}
Вывод
reserved 2 pen
reserved 2 pen
out of stock: 1 pen left
left 1

Разбор#

  • actor Shelf sharded by sku: str — ключ шарда объявлен в заголовке и доступен в теле как неизменяемое поле sku. В литералах записей его нужно писать полностью: Item{ sku: sku, … }.
  • msg take(n: i32) -> Item | OutOfStock — сообщение актора с типизированным результатом, ошибка — часть типа.
  • service Inventory описывает методы; @public открывает их наружу (по умолчанию доступ закрыт, §13.4).
  • impl Inventory { rpc reserve(sku, n) { Shelf(sku).take(n)? } } — типы параметров берутся из контракта; Shelf(sku) — ссылка на актор по ключу. Результат вызова актора содержит NetError (актор может быть на другом узле), поэтому ? пробрасывает и его: в контракте reserve тоже можно было бы не писать NetError — он добавляется всегда.
  • cluster shop { role all { serves Inventory; hosts Shelf } } — единственная роль all обслуживает сервис и размещает акторов. В кластере из многих узлов вы разделили бы роли: api обслуживает сервисы, storage размещает акторов.
  • В main результат разбирается по всем типам: Item, OutOfStock и NetError.
  • Состояние пережило вызовы: после двух резервов осталось 1 штука, третий запрос получил OutOfStock. Поля var актора сохраняются в журнал до отправки ответа, поэтому состояние переживает перезапуск (§12.5).

Типичные ошибки#

Вызов метода, которого нет в контракте — unknown-method:

oxl✓ проверяется в CI
service Users {
  @public rpc count() -> i32
}

impl Users {
  rpc count() { 2 }
}

fn main() io, net {
  let n = Users.total()
}
error
unknown-method

Если в программе есть cluster, а сервис не назначен ни одной роли (serves …), сборка остановится на service-not-served — иначе такой сервис просто никто бы не поднял.

Что запомнить#

  • service = контракт, impl = реализация, actor = состояние, cluster = размещение.
  • Вызов не знает, где живёт реализация; сетевые ошибки всегда видны в типе результата.
  • Начинайте с одного процесса; разносите по узлам, когда это нужно.

Что дальше#

См. также

OxL 1.0.0-beta · Версия документации 1.0.0-beta© OxwaldLink · сгенерировано из Стандарта, исходников std и руководства