diff --git a/python/basics/README.md b/python/basics/README.md index 0d58dcbd..d8ebf5f6 100644 --- a/python/basics/README.md +++ b/python/basics/README.md @@ -14,49 +14,29 @@ about how they work and how they can be run. finished actions. The example applies a series of updates and permission setting changes to user's profile. -* **[Durable Execution with Compensations](app/2_durable_execution_compensation.py):** - Reliably compensating / undoing previous actions upon unrecoverable errors halfway - through multi-step change. This is the same example as above, extended for cases where - a part of the change cannot be applied (conflict) and everything has to roll back. - -* **[Workflows](app/3_workflows.py):** Workflows are durable execution tasks that can +* **[Workflows](app/2_workflows.py):** Workflows are durable execution tasks that can be submitted and awaited. They have an identity and can be signaled and queried through durable promises. The example is a user-signup flow that takes multiple operations, including verifying the email address. -* **[Virtual Objects](app/4_virtual_objects.py):** Stateful serverless objects +* **[Virtual Objects](app/3_virtual_objects.py):** Stateful serverless objects to manage durable consistent state and state-manipulating logic. -* **[Kafka Event-processing](app/5_events_processing.py):** Processing events to - update various downstream systems with durable event handlers, event-delaying, - in a strict-per-key order. - -* **[Stateful Event-processing](app/6_events_state.py):** Populating state from - events and making is queryable via RPC handlers. - - ### Running the examples To set up the example, use the following sequence of commands. 1. Setup the virtual env: ```shell - python3 -m venv .venv - source .venv/bin/activate - ``` - -2. Install the requirements: - ```shell + python3 -m venv venv + source venv/bin/activate pip install -r requirements.txt ``` 3. Start the app as follows: - Durable execution example: `python -m hypercorn --config hypercorn-config.toml app/1_durable_execution.py:app` - - Durable execution with compensations example: `python -m hypercorn --config hypercorn-config.toml app/2_durable_execution_compensation.py:app` - - Workflows example: `python -m hypercorn --config hypercorn-config.toml app/3_workflows.py:app` - - Virtual Objects example: `python -m hypercorn --config hypercorn-config.toml app/4_virtual_objects.py:app` - - Kafka Event-processing example: `python -m hypercorn --config hypercorn-config.toml app/5_events_processing.py:app` - - Stateful Event-processing example: `python -m hypercorn --config hypercorn-config.toml app/6_events_state.py:app` + - Workflows example: `python -m hypercorn --config hypercorn-config.toml app/2_workflows.py:app` + - Virtual Objects example: `python -m hypercorn --config hypercorn-config.toml app/3_virtual_objects.py:app` 4. Start the Restate Server ([other options here](https://docs.restate.dev/develop/local_dev)): ```shell diff --git a/python/basics/app/1_durable_execution.py b/python/basics/app/1_durable_execution.py index b7135788..529ce0fb 100644 --- a/python/basics/app/1_durable_execution.py +++ b/python/basics/app/1_durable_execution.py @@ -1,75 +1,55 @@ -# -# Copyright (c) 2023-2024 - Restate Software, Inc., Restate GmbH -# -# This file is part of the Restate examples, -# which is released under the MIT license. -# -# You can find a copy of the license in file LICENSE in the root -# directory of this repository or package, or at -# https:#github.com/restatedev/sdk-typescript/blob/main/LICENSE -from typing import TypedDict +import uuid -from restate import Service, Context import restate +from pydantic import BaseModel +from restate import Service, Context +from utils import create_recurring_payment, create_subscription -from utils import apply_user_role, apply_permission, UpdateRequest +# Restate lets you implement resilient applications. +# Restate ensures handler code runs to completion despite failures: +# - Automatic retries +# - Restate tracks the progress of execution, and prevents re-execution of completed work on retries +# - Regular code and control flow, no custom DSLs -role_update = Service("roleUpdate") +# Applications consist of services with handlers that can be called over HTTP or Kafka. +subscription_service = Service("SubscriptionService") -# This is an example of the benefits of Durable Execution. -# Durable Execution ensures code runs to the end, even in the presence of -# failures. This is particularly useful for code that updates different systems and needs to -# make sure all updates are applied: -# -# - Failures are automatically retried, unless they are explicitly labeled -# as terminal errors -# - Restate tracks execution progress in a journal. -# Work that has already been completed is not repeated during retries. -# Instead, the previously completed journal entries are replayed. -# This ensures that stable deterministic values are used during execution. -# - Durable executed functions use the regular code and control flow, -# no custom DSLs -# +class SubscriptionRequest(BaseModel): + user_id: str + credit_card: str + subscriptions: list[str] -@role_update.handler(name="applyRoleUpdate") -async def apply_role_update(ctx: Context, update: UpdateRequest): - # parameters are durable across retries - user_id, role, permissions = update["user_id"], update["role"], update["permissions"] - # Apply a change to one system (e.g., DB upsert, API call, ...). - # The side effect persists the result with a consensus method so - # any later code relies on a deterministic result. - success = await ctx.run("apply_user_role", lambda: apply_user_role(user_id, role)) - if not success: - return +@subscription_service.handler() +async def add(ctx: Context, req: SubscriptionRequest): + # Stable idempotency key: Restate persists the result of + # all `ctx` actions and recovers them after failures + payment_id = await ctx.run("payment id", lambda: str(uuid.uuid4())) - # Loop over the permission settings and apply them. - # Each operation through the Restate context is journaled - # and recovery restores results of previous operations from the journal - # without re-executing them. - for permission in permissions: - await ctx.run("apply_permission", lambda: apply_permission(user_id, permission)) + # Retried in case of timeouts, API downtime, etc. + pay_ref = await ctx.run("recurring payment", + lambda: create_recurring_payment(req.credit_card, payment_id)) + # Persists successful subscriptions and skip them on retries + for subscription in req.subscriptions: + await ctx.run("subscription", + lambda: create_subscription(req.user_id, subscription, pay_ref)) -app = restate.app(services=[role_update]) + +# Create an HTTP endpoint to serve your services on port 9080 +# or use .handler() to run on Lambda, Deno, Bun, Cloudflare Workers, ... +app = restate.app([subscription_service]) # -# See README for details on how to start and connect Restate. -# -# When invoking this function (see below for sample request), it will apply all -# role and permission changes, regardless of crashes. -# You will see all lines of the type "Applied permission remove:allow for user Sam Beckett" -# in the log, across all retries. You will also see that re-tries will not re-execute -# previously completed actions again, so each line occurs only once. +# Check the README to learn how to run Restate. +# Then invoke this function and see in the log how it recovers. +# Each action (e.g. "created recurring payment") is only logged once across all retries. +# Retries did not re-execute the successful operations. # -# curl localhost:8080/roleUpdate/applyRoleUpdate -H 'content-type: application/json' -d \ +# curl localhost:8080/SubscriptionService/add -H 'content-type: application/json' -d \ # '{ # "user_id": "Sam Beckett", -# "role": "content-manager", -# "permissions" : [ -# { "permissionKey": "add", "setting": "allow" }, -# { "permissionKey": "remove", "setting": "allow" }, -# { "permissionKey": "share", "setting": "block" } -# ] +# "credit_card": "1234-5678-9012-3456", +# "subscriptions" : ["Netflix", "Disney+", "HBO Max"] # }' diff --git a/python/basics/app/2_durable_execution_compensation.py b/python/basics/app/2_durable_execution_compensation.py deleted file mode 100644 index bd5e028d..00000000 --- a/python/basics/app/2_durable_execution_compensation.py +++ /dev/null @@ -1,87 +0,0 @@ -# -# Copyright (c) 2023-2024 - Restate Software, Inc., Restate GmbH -# -# This file is part of the Restate examples, -# which is released under the MIT license. -# -# You can find a copy of the license in file LICENSE in the root -# directory of this repository or package, or at -# https:#github.com/restatedev/sdk-typescript/blob/main/LICENSE -from typing import TypedDict - -import restate -from restate import Service, Context -from restate.exceptions import TerminalError - -from utils import try_apply_user_role, UpdateRequest, get_current_role, try_apply_permission, Permission - -role_update = Service("roleUpdate") - - -# Durable execution ensures code runs to the end, even in the presence of -# failures. That allows developers to implement error handling with common -# control flow in code. -# -# - This example uses the SAGA pattern: on error, the code undos previous -# operations in reverse order -# - The code uses common exception handling and variables/arrays to remember -# the previous values to restore -# -@role_update.handler(name="applyRoleUpdate") -async def apply_role_update(ctx: Context, update: UpdateRequest): - # parameters are durable across retries - user_id, role, permissions = update["user_id"], update["role"], update["permissions"] - - # Restate does retries for regular failures. - # TerminalErrors, on the other hand, are not retried and are propagated - # back to the caller. - # No permissions were applied so far, so if this fails, - # we propagate the error directly back to the caller. - previous_role = await ctx.run("current_role", lambda: get_current_role(user_id)) - await ctx.run("", lambda: try_apply_user_role(user_id, role)) - - # Apply all permissions in order. - # We collect the previous permission settings to reset if the process fails. - previous_permissions = [] - for permission in permissions: - try: - previous = await ctx.run("apply_permission", lambda: try_apply_permission(user_id, permission)) - previous_permissions.append(previous) - except TerminalError as e: - await rollback(ctx, user_id, previous_role, previous_permissions) - raise e - - -app = restate.app(services=[role_update]) - - -async def rollback(ctx: restate.Context, user_id: str, previous_role: str, permissions: list[Permission]): - print(">>> !!! ROLLING BACK CHANGES !!! <<<") - for prev in reversed(permissions): - await ctx.run(f"reapply previous permission {prev}", lambda: try_apply_permission(user_id, prev)) - - await ctx.run("reapply previous role", lambda: try_apply_user_role(user_id, previous_role)) - -# -# See README for details on how to start and connect Restate. -# -# When invoking this function (see below for sample request), you will see that -# all role/permission changes are attempted. Upon an unrecoverable error (like a -# semantic application error), previous operations are reversed. -# -# You will see all lines of the type "Applied permission remove:allow for user Sam Beckett", -# and, in case of a terminal error, their reversal. -# -# This will proceed reliably across the occasional process crash, that we blend in. -# Once an action has completed, it does not get re-executed on retries, so each line occurs only once. -# -# curl localhost:8080/roleUpdate/applyRoleUpdate -H 'content-type: application/json' -d \ -# '{ -# "userId": "Sam Beckett", -# "role": { "roleKey": "content-manager", "roleDescription": "Add/remove documents" }, -# "permissions" : [ -# { "permissionKey": "add", "setting": "allow" }, -# { "permissionKey": "remove", "setting": "allow" }, -# { "permissionKey": "share", "setting": "block" } -# ] -# }' diff --git a/python/basics/app/2_workflows.py b/python/basics/app/2_workflows.py new file mode 100644 index 00000000..7a2c2009 --- /dev/null +++ b/python/basics/app/2_workflows.py @@ -0,0 +1,56 @@ +import uuid +import restate +from pydantic import BaseModel +from restate import Workflow, WorkflowContext, WorkflowSharedContext +from app.utils import create_user_entry, send_email_with_link + + +# Workflow for user signup and email verification. +# - Main workflow in run() method +# - Additional methods interact with the workflow. +# Each workflow instance has a unique ID and runs only once (to success or failure). +user_signup = Workflow("usersignup") + + +class User(BaseModel): + name: str + email: str + + +# --- The workflow logic --- +@user_signup.main() +async def run(ctx: WorkflowContext, user: User) -> bool: + # workflow ID = user ID; workflow runs once per user + user_id = ctx.key() + + # Durably executed action; write to other system + async def create_user(): + return await create_user_entry(user) + await ctx.run("create_user", create_user) + + # Send the email with the verification link + secret = await ctx.run("secret", lambda: str(uuid.uuid4())) + await ctx.run("send_email", lambda: send_email_with_link(user_id, user.email, secret)) + + # Wait until user clicked email verification link + # Promise gets resolved or rejected by the other handlers + click_secret = await ctx.promise("email_link").value() + return click_secret == secret + + +# --- Other handlers interact with the workflow via queries and signals --- +@user_signup.handler() +async def click(ctx: WorkflowSharedContext, secret: str): + # Send data to the workflow via a durable promise + await ctx.promise("email_link").resolve(secret) + +app = restate.app(services=[user_signup]) +# You can deploy this as a container, Lambda, etc. - Invoke it over HTTP via: curl +# localhost:8080/usersignup/signup-userid1/run/send -H 'content-type: application/json' \ +# -d '{ "name": "Bob", "email": "bob@builder.com" }' +# +# - Resolve the email link via: +# curl localhost:8080/usersignup/signup-userid1/verifyEmail +# +# - Attach back to the workflow to get the result: +# curl localhost:8080/restate/workflow/usersignup/userid1/attach diff --git a/python/basics/app/4_virtual_objects.py b/python/basics/app/3_virtual_objects.py similarity index 99% rename from python/basics/app/4_virtual_objects.py rename to python/basics/app/3_virtual_objects.py index 7ef04552..d60227e0 100644 --- a/python/basics/app/4_virtual_objects.py +++ b/python/basics/app/3_virtual_objects.py @@ -1,10 +1,6 @@ import restate from restate import VirtualObject, ObjectContext -greeter_object = VirtualObject("greeter") - - -# # Virtual Objects hold state and have methods to interact with the object. # An object is identified by a unique id - only one object exists per id. # @@ -13,7 +9,9 @@ # method execution. # # Virtual Objects are _Stateful Serverless_ constructs. -# +greeter_object = VirtualObject("greeter") + + @greeter_object.handler() async def greet(ctx: ObjectContext, greeting: str) -> str: # Access the state attached to this object (this 'name') diff --git a/python/basics/app/3_workflows.py b/python/basics/app/3_workflows.py deleted file mode 100644 index 02326582..00000000 --- a/python/basics/app/3_workflows.py +++ /dev/null @@ -1,72 +0,0 @@ -import uuid -from typing import TypedDict - -import restate -from restate import Workflow, WorkflowContext, WorkflowSharedContext -from restate.exceptions import TerminalError - -from app.utils import create_user_entry, send_email_with_link - - -class Signup(TypedDict): - user_id: str - email: str - - -user_signup = Workflow("usersignup") - - -@user_signup.main() -async def run(ctx: WorkflowContext, signup: Signup): - - # publish state, for the world to see our progress - ctx.set("stage", "Creating user") - - # use all the standard durable execution features here - await ctx.run("create_user", lambda: create_user_entry(signup)) - - ctx.set("stage", "Verifying email") - - # send the email with the verification secret - secret = await ctx.run("secret", lambda: str(uuid.uuid4())) - await ctx.run("send_email", lambda: send_email_with_link(signup["email"], secret)) - - try: - # the promise here is resolved or rejected by the additional workflow methods below - click_secret = await ctx.promise("email_link").value() - if click_secret != secret: - raise TerminalError("Wrong secret from email link") - except TerminalError as e: - ctx.set("stage", "Email verification failed") - - ctx.set("stage", "Email verified") - - -@user_signup.handler() -async def get_stage(ctx: WorkflowSharedContext) -> str: - # read the state to get the stage where the workflow is - return await ctx.get("stage") - - -@user_signup.handler("verifyEmail") -async def verify_email(ctx: WorkflowContext, secret: str): - # resolve the durable promise to let the awaiter know - await ctx.promise("email_link").resolve(secret) - - -@user_signup.handler("abortVerification") -async def abort_verification(ctx: WorkflowContext): - # failing the durable promise will throw an Error for the awaiting thread - await ctx.promise("email_link").reject("User aborted verification") - - -app = restate.app(services=[user_signup]) - -# You can deploy this as a container, Lambda, etc. -# Invoke it over HTTP via: -# curl localhost:8080/usersignup/signup-userid1/run/send --json '{ "name": "Bob", "email": "bob@builder.com" }' -# -# Resolve the email link via: -# curl localhost:8080/usersignup/signup-userid1/verifyEmail -# Abort the email verification via: -# curl localhost:8080/usersignup/signup-userid1/abortVerification diff --git a/python/basics/app/utils.py b/python/basics/app/utils.py index b918153c..41c412c7 100644 --- a/python/basics/app/utils.py +++ b/python/basics/app/utils.py @@ -1,55 +1,7 @@ import os import random -from typing import TypedDict - -from restate import Context -from restate.exceptions import TerminalError - - -class Permission(TypedDict): - permissionKey: str - setting: str - - -class UpdateRequest(TypedDict): - role: str - user_id: str - permissions: list[Permission] - - -def apply_user_role(user_id, role) -> bool: - maybe_crash(0.3) - print(f"Applied {role} to user {user_id}") - return True - - -def apply_permission(user_id, permission): - maybe_crash(0.2) - print(f"Applied permission {permission} to user {user_id}") - - -def try_apply_user_role(user_id, role): - maybe_crash(0.3) - - if role != "viewer": - application_error(0.3, f"Role {role} is not possible for user {user_id}") - print(f"Applied {role} to user {user_id}") - - -def get_current_role(user_id): - # in this example, the previous role was always just 'viewer' - return "viewer" - - -def try_apply_permission(user_id, permission): - maybe_crash(0.3) - - if permission["setting"] != "blocked": - application_error(0.4, f"Could not apply {permission} for user {user_id} due to a conflict") - print(f"Applied permission {permission['permissionKey']} to user {user_id}") - return Permission(permissionKey=permission["permissionKey"], setting="blocked") - +# Utility to let the service crash with a probability to show how the system recovers. kill_process = bool(os.getenv("CRASH_PROCESS")) @@ -58,38 +10,31 @@ def maybe_crash(probability: float = 0.5) -> None: print("A failure happened!") if kill_process: print("--- CRASHING THE PROCESS ---") - os.exit(1) + raise SystemExit(1) else: raise Exception("A failure happened!") -def application_error(probability: float, message: str) -> None: - if random.random() < probability: - print(f"Action failed: {message}") - raise TerminalError(message) - - -# ======================================================= -# Stubs for 3_workflows.py - -def create_user_entry(signup): - pass - - -def send_email_with_link(param, secret): - pass - +# Simulates calling a subscription API, with a random probability of API downtime. +def create_subscription(user_id: str, subscription: str, _payment_ref: str) -> str: + maybe_crash(0.3) + print(f">>> Creating subscription {subscription} for user {user_id}") + return "SUCCESS" -# ======================================================= -# Stubs for 5_events_processing.py -def setup_user_permissions(user_id, permissions): - pass +# Simulates calling a payment API, with a random probability of API downtime. +def create_recurring_payment(_credit_card: str, payment_id: str) -> str: + maybe_crash(0.3) + print(f">>> Creating recurring payment {payment_id}") + return "payment-reference" -def provision_resources(user_id, role_id, resources): - pass +# Stubs for 2_workflows.py +async def create_user_entry(user): + print(f"Creating user entry for {user}") -def update_user_profile(profile): - pass +def send_email_with_link(user_id: str, email: str, secret: str): + print(f"Sending email to {email} with secret {secret}. \n" + f"To simulate a user clicking the link, run the following command: \n" + f"curl localhost:8080/usersignup/{user_id}/click -H 'content-type: application/json' -d '{{ \"secret\": \"{secret}\"}}'") diff --git a/python/basics/docker-compose.yaml b/python/basics/docker-compose.yaml deleted file mode 100644 index 6f9ee00a..00000000 --- a/python/basics/docker-compose.yaml +++ /dev/null @@ -1,41 +0,0 @@ -version: '3' -services: - broker: - image: confluentinc/cp-kafka:7.5.0 - container_name: broker - ports: - - "9092:9092" - - "9101:9101" - environment: - KAFKA_BROKER_ID: 1 - KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT - KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://broker:29092,PLAINTEXT_HOST://localhost:9092 - KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 - KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0 - KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 - KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 - KAFKA_PROCESS_ROLES: broker,controller - KAFKA_NODE_ID: 1 - KAFKA_CONTROLLER_QUORUM_VOTERS: 1@broker:29093 - KAFKA_LISTENERS: PLAINTEXT://broker:29092,CONTROLLER://broker:29093,PLAINTEXT_HOST://0.0.0.0:9092 - KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT - KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER - KAFKA_LOG_DIRS: /tmp/kraft-combined-logs - CLUSTER_ID: MkU3OEVBNTcwNTJENDM2Qk - - init-kafka: - image: confluentinc/cp-kafka:7.5.0 - depends_on: - - broker - entrypoint: [ '/bin/sh', '-c' ] - command: | - " - # blocks until kafka is reachable - kafka-topics --bootstrap-server broker:29092 --list - echo -e 'Creating kafka topics' - kafka-topics --bootstrap-server broker:29092 --create --if-not-exists --topic users --replication-factor 1 --partitions 6 - kafka-topics --bootstrap-server broker:29092 --create --if-not-exists --topic profiles --replication-factor 1 --partitions 6 - - echo -e 'Successfully created the following topics:' - kafka-topics --bootstrap-server broker:29092 --list - " \ No newline at end of file diff --git a/python/basics/requirements.txt b/python/basics/requirements.txt index 9561699a..a0112ec3 100644 --- a/python/basics/requirements.txt +++ b/python/basics/requirements.txt @@ -1,2 +1,3 @@ hypercorn -restate_sdk==0.4.1 \ No newline at end of file +restate_sdk==0.4.1 +pydantic \ No newline at end of file diff --git a/python/basics/restate.toml b/python/basics/restate.toml deleted file mode 100644 index 8a0bde1c..00000000 --- a/python/basics/restate.toml +++ /dev/null @@ -1,3 +0,0 @@ -[[ingress.kafka-clusters]] -name = "my-cluster" -brokers = ["PLAINTEXT://localhost:9092"] \ No newline at end of file diff --git a/python/patterns-use-cases/src/statemachinepayments/payment_processor.py b/python/patterns-use-cases/src/statemachinepayments/payment_processor.py index 251eccfa..bfbc2b47 100644 --- a/python/patterns-use-cases/src/statemachinepayments/payment_processor.py +++ b/python/patterns-use-cases/src/statemachinepayments/payment_processor.py @@ -2,6 +2,9 @@ from pydantic import BaseModel from restate import VirtualObject, ObjectContext from datetime import timedelta + +from restate.serde import PydanticJsonSerde, Serde + from accounts.accounts import account import accounts.accounts as account_service from src.statemachinepayments.types import Result @@ -52,7 +55,7 @@ async def make_payment(ctx: ObjectContext, payment: Payment) -> Result: # caller may retry this (with the same payment-id), for the sake of this example if payment_result.success: ctx.set(STATUS, PaymentStatus.COMPLETED_SUCCESSFULLY) - ctx.set(PAYMENT, payment.model_dump()) + ctx.set(PAYMENT, payment, serde=PydanticJsonSerde(Payment)) ctx.object_send(expire, payment_id, send_delay=EXPIRY_TIMEOUT, arg=None) return payment_result @@ -76,7 +79,7 @@ async def cancel_payment(ctx: ObjectContext): ctx.set(STATUS, PaymentStatus.CANCELED) # undo the payment - payment = Payment(**await ctx.get(PAYMENT)) + payment = await ctx.get(PAYMENT, serde=PydanticJsonSerde(Payment)) ctx.object_send(account_service.deposit, key=payment.account_id, arg=payment.amount_cents) diff --git a/typescript/basics/src/utils/stubs.ts b/typescript/basics/src/utils/stubs.ts index d481f833..10888eed 100644 --- a/typescript/basics/src/utils/stubs.ts +++ b/typescript/basics/src/utils/stubs.ts @@ -1,5 +1,3 @@ -import * as restate from "@restatedev/restate-sdk"; - /** * Utility to let the service crash with a probability to show how the system recovers. */ @@ -29,10 +27,10 @@ export type SubscriptionRequest = { export function createSubscription( userId: string, subscription: string, - paymentRef: string, + _paymentRef: string, ): string { maybeCrash(0.3); - console.log(`>>> Creating subscription ${subscription} for user ${userId} with payment ${paymentRef}`); + console.log(`>>> Creating subscription ${subscription} for user ${userId}`); return "SUCCESS"; } @@ -40,7 +38,7 @@ export function createSubscription( * Simulates calling a payment API, with a random probability of API downtime. */ export function createRecurringPayment( - creditCard: string, + _creditCard: string, paymentId: any, ): string { maybeCrash(0.3);