Skip to main content

Calling Reboot methods

Reboot applications are usually consumed via generated Python, TypeScript, or React client code that calls remote methods of your state data types.

References and method calls

To call a method on a state you need to get a reference to it first by calling .lookup(). You can think of a reference like a handle (and it's similar to a "stub" in gRPC parlance). The actual type of reference returned from calling .lookup() is a WeakReference, where the "weak" is indicative of the fact that the state may or may not exist. Here's an example:

from_account = Account.lookup(request.from_account_id)

(You can ignore the context argument for a few paragraphs.)

Every reference is associated with a particular state ID, which uniquely identifies that state and where it is hosted on a Reboot cluster.

If you have references to multiple states at once that you want to update you'll need to do so within a transaction. Here's an example of doing a bank transfer which mutates both the account we withdrawing money from and the account we are depositing money into:

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()

Idempotency

If you'd like a guarantee that a call to a Reboot method happens exactly once, you can call that method using the .idempotently() builder method:

bank = Bank.lookup(SINGLETON_BANK_ID)

await bank.idempotently().SignUp(
context,
customer_name="Initial User",
)

If you need to call the same method on a state more than once, including when you're doing manual retries, then you can specify an idempotency "alias" to distinguish each call, for example:

# Since we may manually retry the following call, we must
# provide idempotency, which we do via passing the string
# alias 'open'.
await Account.construct(
id=request.account_id,
).idempotently('open').Open(
context,
Options(bearer_token=context.bearer_token),
initial_deposit=(
request.initial_deposit
if request.HasField('initial_deposit') else None
),
)

Reboot will deterministically generate an idempotency key given an alias. You can also manually pass an idempotency key by calling .idempotently(key=...).

Scheduling

Method calls can also be spawned as async tasks using the schedule() builder method:

task = await self.lookup().schedule().WelcomeEmailTask(context)

With no arguments to schedule(), your spawned task will run immediately in the background, and will retry until it completes.

You can additionally pass the when argument a timedelta or datetime to schedule the spawning of the task for a point in the future:

await self.lookup().schedule(
when=timedelta(seconds=request.quote_expiration_seconds),
).ExpireQuoteTask(
context,
quote=quote,
)

Reactivity

Calls made from clients or workflow methods to reader methods can also be made reactively, which will cause them to re-execute each time the method's result changes on the server:

fig = Fig.lookup(fig_id)
async for response in fig.reactively().GetPosition(context):
print(f"{fig_id}: {response}")

Contexts

Remember the context parameter from the top of this page? Depending on whether your client is running outside-of or inside-of a Reboot app, it will be either an ExternalContext or a Context. Read on.

From outside of an app

An ExternalContext allows clients running outside of Reboot to call Reboot methods.

For example, to create a new ExternalContext in a unit test, call the create_external_context() method on a Reboot instance:

context = self.rbt.create_external_context(name=f"test-{self.id()}")

That ExternalContext can then be used to call methods on your states:

bank = Bank.lookup("my-bank")

alice: SignUpResponse = await bank.SignUp(
context,
customer_name="Alice",
)

You can also create an ExternalContext with a gateway parameter, allowing you to specify a particular Reboot gateway for the method call:

    context = ExternalContext(
name="external",
gateway="dev.localhost.direct:9991", # Default address.
secure_channel=True,
)

hello = Hello.lookup("hello")

response = await hello.Send(context, message="Hello, World!")

Because an ExternalContext is used by client code outside of Reboot, a series of Reboot methods called using an ExternalContext don't have any atomicity guarantees with regard to one another (although the individual methods executing within Reboot will of course still have their own atomicity). If you are calling multiple Reboot methods that you would like to happen atomically, you can move those calls into a transaction, and then call that transaction method from your client.

From inside of an app

A sub-type of Reboot's Context type is passed into every method implemented by a Reboot state. These server Contexts are used by Reboot states to call other Reboot methods.

Each method kind that can exist on a Reboot state has its own Context sub-type, which it receives as a parameter:

The types of these contexts allow Reboot (as well as static type checkers like mypy, Pyright, or tsc) to enforce its safety guarantees throughout the call graph.

HTTP

Reboot can also be accessed directly via HTTP by clients which don't have access to Reboot generated code. See the interfaces page for more information about Reboot's additional required headers when compared to gRPC.

curl -XPOST https://dev.localhost.direct:9991/hello.v1.HelloInterface/Messages \
-H "x-reboot-state-ref:hello.v1.Hello:hello-reboot"

If you need to include a request body, you can use the -d flag as shown below:

curl -XPOST "https://localhost.direct:9991/hello.v1.HelloInterface/Send" \
-H "x-reboot-state-ref:hello.v1.Hello:reboot-hello" \
-d '{"message":"Hello, World!"}'