diff --git a/examples/example.py b/examples/example.py index fe88cea..ad29396 100644 --- a/examples/example.py +++ b/examples/example.py @@ -15,12 +15,14 @@ import restate from greeter import greeter +from random_greeter import random_greeter from virtual_object import counter from workflow import payment from pydantic_greeter import pydantic_greeter from concurrent_greeter import concurrent_greeter app = restate.app(services=[greeter, + random_greeter, counter, payment, pydantic_greeter, diff --git a/examples/random_greeter.py b/examples/random_greeter.py new file mode 100644 index 0000000..cdb86ff --- /dev/null +++ b/examples/random_greeter.py @@ -0,0 +1,39 @@ +# +# Copyright (c) 2023-2024 - Restate Software, Inc., Restate GmbH +# +# This file is part of the Restate SDK for Python, +# 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 +# +"""example.py""" +# pylint: disable=C0116 +# pylint: disable=W0613 + +from restate import Service, Context + +random_greeter = Service("random_greeter") + +@random_greeter.handler() +async def greet(ctx: Context, name: str) -> str: + + # ctx.random() returns a Python Random instance seeded deterministically. + # By using ctx.random() you don't write entries in the journal, + # but you still get the same generated values on retries. + + # To generate random numbers + random_number = ctx.random().randint(0, 100) + + # To generate random bytes + random_bytes = ctx.random().randbytes(10) + + # Use ctx.uuid() to generate a UUID v4 seeded deterministically + # As with ctx.random(), this won't write entries in the journal + random_uuid = ctx.uuid() + + return (f"Hello {name} with " + f"random number {random_number}, " + f"random bytes {random_bytes!r} " + f"and uuid {random_uuid}!") diff --git a/python/restate/context.py b/python/restate/context.py index 82b9006..f006097 100644 --- a/python/restate/context.py +++ b/python/restate/context.py @@ -14,6 +14,8 @@ """ import abc +from random import Random +from uuid import UUID from dataclasses import dataclass from typing import Any, Awaitable, Callable, Dict, List, Optional, TypeVar, Union, Coroutine, overload, ParamSpec import typing @@ -219,6 +221,21 @@ def request(self) -> Request: Returns the request object. """ + @abc.abstractmethod + def random(self) -> Random: + """ + Returns a Random instance inherently predictable, deterministically seeded by Restate. + + This instance is useful to generate identifiers, idempotency keys, and for uniform sampling from a set of options. + """ + + @abc.abstractmethod + def uuid(self) -> UUID: + """ + Returns a random UUID, deterministically seeded. + + This UUID will be stable across retries and replays. + """ @typing_extensions.deprecated("`run` is deprecated, use `run_typed` instead for better type safety") @overload diff --git a/python/restate/server_context.py b/python/restate/server_context.py index 7e46c79..259e612 100644 --- a/python/restate/server_context.py +++ b/python/restate/server_context.py @@ -18,12 +18,14 @@ import asyncio import contextvars +from random import Random from datetime import timedelta import inspect import functools from typing import Any, Awaitable, Callable, Dict, List, Optional, TypeVar, Union, Coroutine import typing import traceback +from uuid import UUID from restate.context import DurablePromise, AttemptFinishedEvent, HandlerType, ObjectContext, Request, RestateDurableCallFuture, RestateDurableFuture, RunAction, SendHandle, RestateDurableSleepFuture, RunOptions, P from restate.exceptions import TerminalError @@ -278,6 +280,7 @@ def __init__(self, self.invocation = invocation self.attempt_headers = attempt_headers self.send = send + self.random_instance = Random(invocation.random_seed) self.receive = receive self.run_coros_to_execute: dict[int, Callable[[], Awaitable[None]]] = {} self.request_finished_event = asyncio.Event() @@ -471,6 +474,12 @@ def request(self) -> Request: attempt_finished_event=ServerTeardownEvent(self.request_finished_event), ) + def random(self) -> Random: + return self.random_instance + + def uuid(self) -> UUID: + return UUID(int=self.random_instance.getrandbits(128), version=4) + # pylint: disable=R0914 async def create_run_coroutine(self, handle: int,