Шаг 12. Маленький сервис
Зачем#
Большинство программ на OxL — это сервисы: набор операций, которые вызывают другие программы. Обычно для этого приходится выбирать фреймворк, описывать схему сообщений, писать клиент, настраивать маршрутизацию, обнаружение узлов и сохранение состояния. В OxL всё это — части языка:
service— контракт: списокrpc-методов, их параметры, результаты и ошибки;impl— реализация контракта;actor— состояние;sharded by— по экземпляру на ключ, распределённый по узлам;cluster— какие роли какие сервисы обслуживают и какие акторы размещают.
Вызов Inventory.reserve("pen", 2) выглядит одинаково, живёт ли реализация в этом же процессе или на другом узле. Поэтому
сервис можно сначала написать и протестировать в одном процессе, а потом разнести по кластеру, не меняя вызывающий код.
Пример#
Склад: резервирование товара. Состояние (сколько осталось) хранит шардированный актор, по одному на артикул; сервис — вход снаружи.
@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:
service Users {
@public rpc count() -> i32
}
impl Users {
rpc count() { 2 }
}
fn main() io, net {
let n = Users.total()
}unknown-method
Если в программе есть cluster, а сервис не назначен ни одной роли (serves …), сборка остановится на
service-not-served — иначе такой сервис просто никто бы не поднял.
Что запомнить#
service= контракт,impl= реализация,actor= состояние,cluster= размещение.- Вызов не знает, где живёт реализация; сетевые ошибки всегда видны в типе результата.
- Начинайте с одного процесса; разносите по узлам, когда это нужно.
Что дальше#
- Тур: саги, хранилище, кластер, развёртывание.
- Полный пример shop — распределённый магазин.
- Кулинарная книга — десятки готовых рецептов.