Skip to main content

Servicers

Servicers are server-side (backend) classes which handle incoming RPCs for each of the methods that you declared in your .proto files.

To implement the methods for a state type called, e.g., Account, you'll need to subclass the generated Account.Interface class and provide an implementation for the abstract methods declared in that class.

Every method gets passed a context, a subclass of Context specific to the method kind.

All but the workflow methods are also passed the state that the method is operating on, e.g., Account.State for the Account type.

Every method gets passed its request and returns a response of the types declared in the .proto.

If you've used gRPC this should look very familiar, with the difference being the type of context and the inclusion of the state argument for some of the methods.

reader

A reader method is immutable, and any updates you make to the passed in state argument are ignored.

A reader method gets passed a context of type ReaderContext. A ReaderContext can only be used to make calls to other reader methods.

Here's an example of a reader method called Balance on our Account state that returns the account's current balance:

async def Balance(
self,
context: ReaderContext,
state: Account.State,
request: BalanceRequest,
) -> BalanceResponse:
return BalanceResponse(balance=state.balance)

writer

A writer method gets exclusive, atomic access to state in order to update it. Any updates a writer makes to the passed in state argument are persisted after the method returns, but before returning to the caller. If the method fails (or it is part of a transaction which aborts), its changes to state are not persisted.

A writer method gets passed a context of type WriterContext. A WriterContext can only be used to make calls to other reader methods (that is not a typo, writer's can only call other reader's).

note

To replace an entire state you'll need to do state.CopyFrom(...) in Python and state.copyFrom(...) in Node.js.

Here's an example of a writer method called Deposit on our Account state that increments the account's current balance by some amount in the request:

async def Deposit(
self,
context: WriterContext,
state: Account.State,
request: DepositRequest,
) -> DepositResponse:
state.balance += request.amount
return DepositResponse(updated_balance=state.balance)

transaction

A transaction method gets exclusive, atomic access to state in order to update it (similar to a writer). But unlike a writer, a transaction can also call the reader, writer, and transaction methods of other states. Those reads and writes all occur as a (distributed) ACID transaction.

A transaction method gets passed a context of type TransactionContext. A TransactionContext can be used to make calls to reader's, writer's, and other transaction methods (although currently nested calls to a transaction method may only read/write mutually exclusive state.)

note

Just like with writer's, to replace an entire state you'll need to do state.CopyFrom(...) in Python and state.copyFrom(...) in Node.js.

Here's an example of a transaction method called Transfer on our Bank state that deposits money in one account and withdraws it from another:

async def Transfer(
self,
context: TransactionContext,
state: Bank.State,
request: TransferRequest,
) -> TransferResponse:
from_account = Account.lookup(request.from_account_id)
to_account = Account.lookup(request.to_account_id)
await from_account.Withdraw(context, amount=request.amount)
await to_account.Deposit(context, amount=request.amount)
return TransferResponse()

Again, like writers, state can be modified directly in a transaction method, for example here is an example of the SignUp method for Bank that stores the account IDs:

# Transactions like writers can alter state directly.
state.account_ids.append(account.state_id)

workflow

Documentation coming soon!

Constructors

As part of their definitions, states may designate writers or transactions as constructors by giving them an extra annotation.

State data types that have constructor methods must be initialized by a call to one of their constructors. Calls to other methods on an uninitialized state will raise an error. Once a state has been created, calls to constructors on that state are no longer valid and will raise an error (the type system will also prevent you from doing so in the first place).

For example, a state for a bank account might have an Open constructor method that opens a new bank account:

rpc Open(OpenRequest) returns (OpenResponse) {
option (rbt.v1alpha1.method).writer = {
constructor: {},
};
}

As with other methods, constructors may be called idempotently.

Implicit Constructors

If a state data type doesn't specify any explicit constructors, new state instances of that type must be constructed by calling a writer or transaction method which acts as an implicit constructor. In this case, consult the state data type's documentation as it may recommend using a particular method for implicit construction.