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

rpc и акторы

Банковский счёт: сервис, актор и ошибки#

Задача. Описать операции над счётом как контракт, хранить состояние в акторе и вернуть вызывающему типизированные ошибки.

oxl✓ проверяется в CI
@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 операций, безопасно при любом количестве вызывающих.

oxl✓ проверяется в CI
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 возвращает ответ; вызывающий приостанавливается до получения.
  • Тот же приём подходит для кэшей, счётчиков, очередей заданий, сессий: «одна сущность — один актор».
  • Подводный камень: внутри одного сообщения не делайте долгих блокирующих операций — пока оно выполняется, остальные ждут.

Вернуться: кулинарная книга.

См. также

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