OxL Reference

Step 12. A small service

Why#

Most OxL programs are services: a set of operations that other programs call. Usually this means choosing a framework, describing the message schema, writing a client, configuring routing, node discovery and state persistence. In OxL all of this is part of the language:

  • service is the contract: a list of rpc methods, their parameters, results and errors;
  • impl is the implementation of the contract;
  • actor is state; sharded by means one instance per key, spread over nodes;
  • cluster says which roles serve which services and host which actors.

The call Inventory.reserve("pen", 2) looks the same whether the implementation lives in this process or on another node. A service can therefore be written and tested in one process first and then spread over a cluster without changing the calling code.

Example#

A warehouse: reserving goods. The state (how many are left) is kept by a sharded actor, one per article; the service is the entrance from outside.

oxl✓ checked in CI
@error data OutOfStock { sku: str; left: i32 }

data Item { sku: str; qty: i32 }

actor Shelf sharded by sku: str {  // one actor per sku; the state lives in the actor
  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 {  // the contract: what is visible from outside
  @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)? }  // the service delegates to the actor
  rpc stock(sku) { Shelf(sku).count()? }
}

cluster shop {
  role all { serves Inventory; hosts Shelf }  // the node serves the service and hosts the actors
}

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")
  }
}
Output
reserved 2 pen
reserved 2 pen
out of stock: 1 pen left
left 1

Walkthrough#

  • actor Shelf sharded by sku: str - the shard key is declared in the header and is available in the body as an immutable field sku. In record literals it has to be written in full: Item{ sku: sku, … }.
  • msg take(n: i32) -> Item | OutOfStock - an actor message with a typed result; the error is part of the type.
  • service Inventory describes the methods; @public opens them to the outside (access is closed by default, §13.4).
  • impl Inventory { rpc reserve(sku, n) { Shelf(sku).take(n)? } } - parameter types come from the contract; Shelf(sku) is a reference to the actor by key. The result of an actor call contains NetError (the actor may be on another node), so ? passes it on too: in the contract of reserve you need not write NetError - it is always added.
  • cluster shop { role all { serves Inventory; hosts Shelf } } - the single role all serves the service and hosts the actors. In a cluster of many nodes you would split the roles: api serves the services, storage hosts the actors.
  • In main the result is taken apart by all types: Item, OutOfStock and NetError.
  • The state survived the calls: after two reservations 1 is left, the third request got OutOfStock. var fields of an actor are saved to the journal before the answer is sent, so the state survives a restart (§12.5).

Common mistakes#

Calling a method that is not in the contract - unknown-method:

oxl✓ checked in CI
service Users {
  @public rpc count() -> i32
}

impl Users {
  rpc count() { 2 }
}

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

If a program has a cluster and a service is assigned to no role (serves …), the build stops with service-not-served - otherwise nobody would start such a service.

Remember#

  • service = contract, impl = implementation, actor = state, cluster = placement.
  • A call does not know where the implementation lives; network errors are always visible in the result type.
  • Start with one process; spread over nodes when needed.

What next#

See also

OxL 1.0.0-beta · Documentation version 1.0.0-beta© OxwaldLink · generated from the Standard, the std sources and the guides