rpc и акторы
Банковский счёт: сервис, актор и ошибки#
Задача. Описать операции над счётом как контракт, хранить состояние в акторе и вернуть вызывающему типизированные ошибки.
@error data InsufficientFunds { need: i64; have: i64 }
@error data UnknownAccount { id: str }
service Bank {
@public rpc balance(id: str) -> i64 | UnknownAccount
@public rpc withdraw(id: str, amount: i64) -> i64 | UnknownAccount | InsufficientFunds
}
actor Account sharded by id: str {
var cents: i64 = 100
msg get() -> i64 { cents }
msg take(n: i64) -> i64 | InsufficientFunds {
if cents < n { fail InsufficientFunds{ need: n, have: cents } }
cents -= n
cents
}
}
impl Bank {
rpc balance(id) {
if id.isEmpty() { fail UnknownAccount{ id } }
Account(id).get()?
}
rpc withdraw(id, amount) {
if id.isEmpty() { fail UnknownAccount{ id } }
Account(id).take(amount)?
}
}
cluster bank {
role all { serves Bank; hosts Account }
}
fn show(label: str, r: i64 | UnknownAccount | InsufficientFunds | NetError) io {
match r {
i64 n => print("{label}: ok {n}")
UnknownAccount e => print("{label}: unknown account '{e.id}'")
InsufficientFunds e => print("{label}: need {e.need}, have {e.have}")
NetError => print("{label}: network error") // NetError добавляется к результату любого rpc
}
}
fn main() io, net {
show("balance", Bank.balance("ann"))
show("withdraw 30", Bank.withdraw("ann", 30))
show("withdraw 90", Bank.withdraw("ann", 90))
show("no account", Bank.balance(""))
}Вывод
balance: ok 100 withdraw 30: ok 70 withdraw 90: need 90, have 70 no account: unknown account ''
@error data InsufficientFunds { need; have }— ошибка с данными: клиент увидит не «ошибку», а факты, на которых может принять решение.service Bank— контракт;actor Account sharded by id: str— по экземпляру на ключ. Состояниеcentsсохраняется в журнал и переживает перезапуск узла.impl Bank { rpc withdraw(id, amount) { Account(id).take(amount)? } }— типы параметров берутся из контракта;?передаёт ошибку актора вызывающему.show(label, r)принимает объединение всех возможных исходов иmatchего разбирает: если в контракт добавить ещё одну ошибку, компилятор потребует обновить всеmatch.NetErrorдобавляется к результату любого rpc: в тестах и на одном узле он не возникает, но в кластере сеть может подвести, и код обязан это учитывать.- Один и тот же код работает в процессе (как здесь) и в кластере из многих узлов — меняется только
cluster-объявление.
Актор как ограничитель частоты#
Задача. Разрешить не больше N операций, безопасно при любом количестве вызывающих.
actor RateLimiter {
var used: i32 = 0
msg tryTake(limit: i32) -> bool {
if used >= limit {
false // лимит исчерпан
} else {
used += 1
true
}
}
msg reset() { used = 0 }
}
fn main() io {
let rl = spawn RateLimiter()
let a = rl.tryTake(2) // сообщения обрабатываются по одному, поэтому гонок нет
let b = rl.tryTake(2)
let c = rl.tryTake(2)
print("{a} {b} {c}")
rl.reset()
let d = rl.tryTake(2)
print("after reset: {d}")
}Вывод
true true false after reset: true
- Состояние
usedдоступно только изmsg-методов актора, а сообщения обрабатываются по одному — поэтому счётчик корректен без мьютексов, сколько бы задач ни обращалось к актору. msg tryTake(limit) -> boolвозвращает ответ; вызывающий приостанавливается до получения.- Тот же приём подходит для кэшей, счётчиков, очередей заданий, сессий: «одна сущность — один актор».
- Подводный камень: внутри одного сообщения не делайте долгих блокирующих операций — пока оно выполняется, остальные ждут.
Вернуться: кулинарная книга.