From 7b068cfbb0bb17ff83d3c929bdf450be67e4d46e Mon Sep 17 00:00:00 2001 From: Giselle van Dongen Date: Mon, 9 Sep 2024 17:05:26 +0200 Subject: [PATCH] Python docs (#454) --- code_snippets/go/develop/serving.go | 2 +- .../main/java/develop/ServingIdentity.java | 3 +- .../main/kotlin/develop/ServingIdentity.kt | 3 +- code_snippets/python/src/__init__.py | 0 code_snippets/python/src/develop/__init__.py | 0 .../python/src/develop/awakeables.py | 35 ++++ .../python/src/develop/durable_timers.py | 12 ++ .../python/src/develop/error_handling.py | 13 ++ .../python/src/develop/journaling_results.py | 15 ++ .../python/src/develop/my_service.py | 11 ++ .../python/src/develop/my_virtual_object.py | 16 ++ .../python/src/develop/my_workflow.py | 22 +++ .../python/src/develop/serialization.py | 58 +++++++ .../src/develop/service_communication.py | 59 +++++++ code_snippets/python/src/develop/serving.py | 16 ++ code_snippets/python/src/develop/state.py | 27 ++++ .../src/develop/workflows/email_client.py | 14 ++ .../python/src/develop/workflows/service.py | 19 +++ .../python/src/develop/workflows/signup.py | 50 ++++++ .../python/src/get_started/__init__.py | 0 .../python/src/get_started/checkout.py | 6 +- code_snippets/python/src/get_started/tour.py | 3 - code_snippets/ts/src/develop/awakeable.ts | 11 +- code_snippets/ts/src/develop/serving.ts | 3 +- docs/deploy/cloud.md | 30 +--- docs/develop/go/state.mdx | 2 +- docs/develop/java/overview.mdx | 4 +- docs/develop/java/state.mdx | 2 +- docs/develop/python/_category_.json | 7 + docs/develop/python/awakeables.mdx | 97 ++++++++++++ docs/develop/python/durable-timers.mdx | 42 +++++ docs/develop/python/error-handling.mdx | 20 +++ docs/develop/python/journaling-results.mdx | 35 ++++ docs/develop/python/overview.mdx | 77 +++++++++ docs/develop/python/serialization.mdx | 30 ++++ docs/develop/python/service-communication.mdx | 149 ++++++++++++++++++ docs/develop/python/serving.mdx | 33 ++++ docs/develop/python/state.mdx | 62 ++++++++ docs/develop/python/workflows.mdx | 136 ++++++++++++++++ docs/develop/ts/overview.mdx | 4 +- docs/develop/ts/serving.mdx | 8 +- docs/develop/ts/state.mdx | 2 +- docs/develop/ts/workflows.mdx | 2 +- docs/get_started/tour.mdx | 6 +- docs/overview.mdx | 12 ++ src/plugins/code-loader.js | 39 ++++- static/img/go.svg | 61 +------ static/img/java.svg | 7 +- static/img/python.svg | 93 +++++++++++ 49 files changed, 1225 insertions(+), 133 deletions(-) create mode 100644 code_snippets/python/src/__init__.py create mode 100644 code_snippets/python/src/develop/__init__.py create mode 100644 code_snippets/python/src/develop/awakeables.py create mode 100644 code_snippets/python/src/develop/durable_timers.py create mode 100644 code_snippets/python/src/develop/error_handling.py create mode 100644 code_snippets/python/src/develop/journaling_results.py create mode 100644 code_snippets/python/src/develop/my_service.py create mode 100644 code_snippets/python/src/develop/my_virtual_object.py create mode 100644 code_snippets/python/src/develop/my_workflow.py create mode 100644 code_snippets/python/src/develop/serialization.py create mode 100644 code_snippets/python/src/develop/service_communication.py create mode 100644 code_snippets/python/src/develop/serving.py create mode 100644 code_snippets/python/src/develop/state.py create mode 100644 code_snippets/python/src/develop/workflows/email_client.py create mode 100644 code_snippets/python/src/develop/workflows/service.py create mode 100644 code_snippets/python/src/develop/workflows/signup.py create mode 100644 code_snippets/python/src/get_started/__init__.py create mode 100644 docs/develop/python/_category_.json create mode 100644 docs/develop/python/awakeables.mdx create mode 100644 docs/develop/python/durable-timers.mdx create mode 100644 docs/develop/python/error-handling.mdx create mode 100644 docs/develop/python/journaling-results.mdx create mode 100644 docs/develop/python/overview.mdx create mode 100644 docs/develop/python/serialization.mdx create mode 100644 docs/develop/python/service-communication.mdx create mode 100644 docs/develop/python/serving.mdx create mode 100644 docs/develop/python/state.mdx create mode 100644 docs/develop/python/workflows.mdx create mode 100644 static/img/python.svg diff --git a/code_snippets/go/develop/serving.go b/code_snippets/go/develop/serving.go index 4cbee1ef..f32e8fb4 100644 --- a/code_snippets/go/develop/serving.go +++ b/code_snippets/go/develop/serving.go @@ -32,7 +32,7 @@ func serving() { // if err := server.NewRestate(). Bind(restate.Reflect(MyService{})). - Bind(restate.Reflect(MyVirtualObject{})). + // withClass highlight-line WithIdentityV1("publickeyv1_w7YHemBctH5Ck2nQRQ47iBBqhNHy4FV7t2Usbye2A6f"). Start(context.Background(), ":9080"); err != nil { log.Fatal(err) diff --git a/code_snippets/java/src/main/java/develop/ServingIdentity.java b/code_snippets/java/src/main/java/develop/ServingIdentity.java index ee0082ab..224c04cf 100644 --- a/code_snippets/java/src/main/java/develop/ServingIdentity.java +++ b/code_snippets/java/src/main/java/develop/ServingIdentity.java @@ -8,8 +8,7 @@ class MySecureApp { public static void main(String[] args) { RestateHttpEndpointBuilder.builder() .bind(new MyService()) - .bind(new MyVirtualObject()) - .bind(new MyWorkflow()) + // withClass(1:3) highlight-line .withRequestIdentityVerifier( RestateRequestIdentityVerifier.fromKeys( "publickeyv1_w7YHemBctH5Ck2nQRQ47iBBqhNHy4FV7t2Usbye2A6f")) diff --git a/code_snippets/kotlin/src/main/kotlin/develop/ServingIdentity.kt b/code_snippets/kotlin/src/main/kotlin/develop/ServingIdentity.kt index 35e52922..ae4ddca1 100644 --- a/code_snippets/kotlin/src/main/kotlin/develop/ServingIdentity.kt +++ b/code_snippets/kotlin/src/main/kotlin/develop/ServingIdentity.kt @@ -7,8 +7,7 @@ import dev.restate.sdk.http.vertx.RestateHttpEndpointBuilder fun main() { RestateHttpEndpointBuilder.builder() .bind(MyService()) - .bind(MyVirtualObject()) - .bind(MyWorkflow()) + // withClass(1:5) highlight-line .withRequestIdentityVerifier( RestateRequestIdentityVerifier.fromKeys( "publickeyv1_w7YHemBctH5Ck2nQRQ47iBBqhNHy4FV7t2Usbye2A6f", diff --git a/code_snippets/python/src/__init__.py b/code_snippets/python/src/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/code_snippets/python/src/develop/__init__.py b/code_snippets/python/src/develop/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/code_snippets/python/src/develop/awakeables.py b/code_snippets/python/src/develop/awakeables.py new file mode 100644 index 00000000..a5063380 --- /dev/null +++ b/code_snippets/python/src/develop/awakeables.py @@ -0,0 +1,35 @@ +from restate import Service, Context + +my_service = Service("MyService") + + +@my_service.handler() +async def my_handler(ctx: Context, arg): + + # + # + name, promise = ctx.awakeable() + # + + # + await ctx.run("trigger task", trigger_task_and_deliver_id(name)) + # + + # + payload = await promise + # + # + + # + ctx.resolve_awakeable(name, payload) + # + + # + ctx.reject_awakeable(name, "My error reason") + # + + return arg + + +def trigger_task_and_deliver_id(awakeable_id): + return "123" \ No newline at end of file diff --git a/code_snippets/python/src/develop/durable_timers.py b/code_snippets/python/src/develop/durable_timers.py new file mode 100644 index 00000000..bb23cd07 --- /dev/null +++ b/code_snippets/python/src/develop/durable_timers.py @@ -0,0 +1,12 @@ +from datetime import timedelta + +from restate import Service, Context + +my_service = Service("MyService") + + +@my_service.handler() +async def my_handler(ctx: Context, arg): + # + await ctx.sleep(delta=timedelta(seconds=10)) + # diff --git a/code_snippets/python/src/develop/error_handling.py b/code_snippets/python/src/develop/error_handling.py new file mode 100644 index 00000000..c7b1aa93 --- /dev/null +++ b/code_snippets/python/src/develop/error_handling.py @@ -0,0 +1,13 @@ +from datetime import timedelta + +from restate import Service, Context +from restate.exceptions import TerminalError + +my_service = Service("MyService") + + +@my_service.handler() +async def my_handler(ctx: Context, arg): + # + raise TerminalError("Something went wrong.") + # diff --git a/code_snippets/python/src/develop/journaling_results.py b/code_snippets/python/src/develop/journaling_results.py new file mode 100644 index 00000000..b5f2e7e4 --- /dev/null +++ b/code_snippets/python/src/develop/journaling_results.py @@ -0,0 +1,15 @@ +from restate import Service, Context + +my_service = Service("MyService") + + +@my_service.handler() +async def my_handler(ctx: Context, arg): + # + async def do_db_request(): + # ... implement ... + return "my_result" + + # withClass highlight-line + result = await ctx.run("database request", do_db_request) + # diff --git a/code_snippets/python/src/develop/my_service.py b/code_snippets/python/src/develop/my_service.py new file mode 100644 index 00000000..edbf7695 --- /dev/null +++ b/code_snippets/python/src/develop/my_service.py @@ -0,0 +1,11 @@ +import restate +from restate import Context, Service + +my_service = Service("MyService") + + +@my_service.handler("myHandler") +async def my_service_handler(ctx: Context, greeting: str) -> str: + return f"${greeting}!" + +app = restate.app(services=[my_service]) \ No newline at end of file diff --git a/code_snippets/python/src/develop/my_virtual_object.py b/code_snippets/python/src/develop/my_virtual_object.py new file mode 100644 index 00000000..1f301119 --- /dev/null +++ b/code_snippets/python/src/develop/my_virtual_object.py @@ -0,0 +1,16 @@ +import restate +from restate import VirtualObject, ObjectContext, ObjectSharedContext + +my_virtual_object = VirtualObject("MyVirtualObject") + + +@my_virtual_object.handler("myHandler") +async def my_object_handler(ctx: ObjectContext, greeting: str) -> str: + return f"${greeting} ${ctx.key()}!" + + +@my_virtual_object.handler(kind="shared") +async def my_concurrent_handler(ctx: ObjectSharedContext, greeting: str) -> str: + return f"${greeting} ${ctx.key()}!" + +app = restate.app(services=[my_virtual_object]) diff --git a/code_snippets/python/src/develop/my_workflow.py b/code_snippets/python/src/develop/my_workflow.py new file mode 100644 index 00000000..314a62e6 --- /dev/null +++ b/code_snippets/python/src/develop/my_workflow.py @@ -0,0 +1,22 @@ +import restate +from restate import Workflow, WorkflowSharedContext, WorkflowContext +from restate.serde import Serde + +my_workflow = Workflow("MyWorkflow") + + +@my_workflow.main() +async def run(ctx: WorkflowContext, req: str) -> str: + # ... implement workflow logic here --- + return "success" + + +@my_workflow.handler() +async def interact_with_workflow(ctx: WorkflowSharedContext, req: str): + # ... implement interaction logic here ... + return + +app = restate.app(services=[my_workflow]) + + +Serde diff --git a/code_snippets/python/src/develop/serialization.py b/code_snippets/python/src/develop/serialization.py new file mode 100644 index 00000000..4b425172 --- /dev/null +++ b/code_snippets/python/src/develop/serialization.py @@ -0,0 +1,58 @@ +import json +import typing + +from restate.serde import Serde +from restate import ObjectContext, VirtualObject + + +# +class MyData(typing.TypedDict): + """Represents a response from the GPT model.""" + some_value: str + my_number: int + + +class MySerde(Serde[MyData]): + def deserialize(self, buf: bytes) -> typing.Optional[MyData]: + if not buf: + return None + data = json.loads(buf) + return MyData(some_value=data["some_value"], my_number=data["some_number"]) + + def serialize(self, obj: typing.Optional[MyData]) -> bytes: + if obj is None: + return bytes() + data = { + "some_value": obj["some_value"], + "some_number": obj["my_number"] + } + return bytes(json.dumps(data), "utf-8") +# + + +my_object = VirtualObject("MyService") + + +# +# For the input/output serialization of your handlers +@my_object.handler(input_serde=MySerde(), output_serde=MySerde()) +async def my_handler(ctx: ObjectContext, greeting: str) -> str: + + # To serialize state + await ctx.get("my_state", serde=MySerde()) + ctx.set("my_state", MyData(some_value="value", my_number=123), serde=MySerde()) + + # To serialize awakeable payloads + ctx.awakeable(serde=MySerde()) + + # To serialize the results of actions + await ctx.run("some-task", some_task, serde=MySerde()) + + # etc. + + return "some-output" +# + + +def some_task() -> MyData: + return MyData(some_value="value", my_number=123) diff --git a/code_snippets/python/src/develop/service_communication.py b/code_snippets/python/src/develop/service_communication.py new file mode 100644 index 00000000..7b669c78 --- /dev/null +++ b/code_snippets/python/src/develop/service_communication.py @@ -0,0 +1,59 @@ +from datetime import timedelta + +from restate import Service, Context, VirtualObject, ObjectContext +# +from src.develop.my_service import my_service_handler +# +from src.develop.my_virtual_object import my_object_handler +from src.develop.my_workflow import run, interact_with_workflow + +caller = Service("Caller") + + +@caller.handler() +async def calling_handler(ctx: Context, arg): + # + response = await ctx.service_call(my_service_handler, arg="Hi") + # + + # + response = await ctx.object_call(my_object_handler, key="Mary", arg="Hi") + # + + # + ctx.service_send(my_service_handler, arg="Hi") + # + + # + ctx.object_send(my_object_handler, key="Mary", arg="Hi") + # + + # + ctx.service_send(my_service_handler, arg="Hi", send_delay=timedelta(seconds=5)) + # + + # + ctx.object_send(my_object_handler, key="Mary", arg="Hi", send_delay=timedelta(seconds=5)) + # + + # + ctx.object_send(my_object_handler, key="Mary", arg="I'm call A") + ctx.object_send(my_object_handler, key="Mary", arg="I'm call B") + # + + +@caller.handler() +async def call_workflows(ctx: Context, arg): + # + # Call the `run` handler of the workflow(only works once). + await ctx.workflow_call(run, key="my_workflow_id", arg="Hi") + # Call some other `interact_with_workflow` handler of the workflow. + await ctx.workflow_call(interact_with_workflow, key="my_workflow_id", arg=None) + # + + # + # Call the `run` handler of the workflow (only works once). + ctx.workflow_send(run, key="my_workflow_id", arg="Hi") + # Call some other `interact_with_workflow` handler of the workflow. + ctx.workflow_send(interact_with_workflow, key="my_workflow_id", arg=None) + # diff --git a/code_snippets/python/src/develop/serving.py b/code_snippets/python/src/develop/serving.py new file mode 100644 index 00000000..b9d714a9 --- /dev/null +++ b/code_snippets/python/src/develop/serving.py @@ -0,0 +1,16 @@ +from src.develop.my_service import my_service +from src.develop.my_virtual_object import my_virtual_object + +# +import restate +app = restate.app(services=[my_service, my_virtual_object]) +# + + +# +app = restate.app( + services=[my_service], + # withClass highlight-line + identity_keys=["publickeyv1_w7YHemBctH5Ck2nQRQ47iBBqhNHy4FV7t2Usbye2A6f"] +) +# diff --git a/code_snippets/python/src/develop/state.py b/code_snippets/python/src/develop/state.py new file mode 100644 index 00000000..ca231afa --- /dev/null +++ b/code_snippets/python/src/develop/state.py @@ -0,0 +1,27 @@ +from restate import ObjectContext, VirtualObject + +my_object = VirtualObject("Caller") + + +@my_object.handler() +async def caller(ctx: ObjectContext, arg): + # + state_keys = ctx.state_keys() + # + + # + my_string = await ctx.get("my-string-key") or "default-key" + my_number = await ctx.get("my-number-key") or 123 + # + + # + ctx.set("my-key", "my-new-value") + # + + # + ctx.clear("my-key") + # + + # + ctx.clear_all() + # diff --git a/code_snippets/python/src/develop/workflows/email_client.py b/code_snippets/python/src/develop/workflows/email_client.py new file mode 100644 index 00000000..a2d37b83 --- /dev/null +++ b/code_snippets/python/src/develop/workflows/email_client.py @@ -0,0 +1,14 @@ +# Copyright (c) 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 the file LICENSE +# in the root directory of this repository or package or at +# https://github.com/restatedev/examples/ + +class EmailClient: + + def send_email_with_link(self, email: str, secret: str): + # send the email + return \ No newline at end of file diff --git a/code_snippets/python/src/develop/workflows/service.py b/code_snippets/python/src/develop/workflows/service.py new file mode 100644 index 00000000..e6b7d9be --- /dev/null +++ b/code_snippets/python/src/develop/workflows/service.py @@ -0,0 +1,19 @@ +from restate import VirtualObject, ObjectContext + +from src.develop.workflows.signup import run, get_status + +user_management_object = VirtualObject("UserManagementObject") + + +# +@user_management_object.handler() +async def signup_user(ctx: ObjectContext, email: str): + # focus + result = await ctx.workflow_call(run, key="someone", arg=email) + + +@user_management_object.handler() +async def query_status(ctx: ObjectContext): + # focus + status = await ctx.workflow_call(get_status, key="someone", arg=None) +# diff --git a/code_snippets/python/src/develop/workflows/signup.py b/code_snippets/python/src/develop/workflows/signup.py new file mode 100644 index 00000000..d8707288 --- /dev/null +++ b/code_snippets/python/src/develop/workflows/signup.py @@ -0,0 +1,50 @@ +import uuid + +import restate +from restate import Workflow, WorkflowContext, WorkflowSharedContext +from src.develop.workflows.email_client import EmailClient + + +# +signup_workflow = Workflow("SignupWorkflow") +email_client = EmailClient() + + +# +@signup_workflow.main() +async def run(ctx: WorkflowContext, email: str): + secret = await ctx.run("secret", lambda: str(uuid.uuid4())) + ctx.set("onboarding_status", "Generated secret") + + await ctx.run( + "send email", + lambda: email_client.send_email_with_link(email, secret) + ) + ctx.set("onboarding_status", "Sent email") + + # + click_secret = await ctx.promise("email.clicked").value() + # + ctx.set("onboarding_status", "Clicked email") + + return click_secret == secret +# + + +@signup_workflow.handler() +async def click(ctx: WorkflowSharedContext, secret: str): + # + await ctx.promise("email.clicked").resolve(secret) + # + + +# +@signup_workflow.handler() +async def get_status(ctx: WorkflowSharedContext): + return await ctx.get("onboarding_status") +# + +# +app = restate.app(services=[signup_workflow]) +# +# diff --git a/code_snippets/python/src/get_started/__init__.py b/code_snippets/python/src/get_started/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/code_snippets/python/src/get_started/checkout.py b/code_snippets/python/src/get_started/checkout.py index 5b7eace8..e3d965b8 100644 --- a/code_snippets/python/src/get_started/checkout.py +++ b/code_snippets/python/src/get_started/checkout.py @@ -2,9 +2,9 @@ from restate import Service, ObjectContext -from auxiliary.email_client import EmailClient -from auxiliary.payment_client import PaymentClient -from tour import Order +from src.get_started.auxiliary.email_client import EmailClient +from src.get_started.auxiliary.payment_client import PaymentClient +from src.get_started.tour import Order payment_client = PaymentClient() email_client = EmailClient() diff --git a/code_snippets/python/src/get_started/tour.py b/code_snippets/python/src/get_started/tour.py index b9c16973..5275285f 100644 --- a/code_snippets/python/src/get_started/tour.py +++ b/code_snippets/python/src/get_started/tour.py @@ -14,9 +14,6 @@ from restate.context import ObjectContext, Serde from restate.service import Service -from auxiliary.email_client import EmailClient -from auxiliary.payment_client import PaymentClient - cart = VirtualObject("CartObject") diff --git a/code_snippets/ts/src/develop/awakeable.ts b/code_snippets/ts/src/develop/awakeable.ts index f000792a..dd9d3895 100644 --- a/code_snippets/ts/src/develop/awakeable.ts +++ b/code_snippets/ts/src/develop/awakeable.ts @@ -6,25 +6,24 @@ const service = restate.service({ greet: async (ctx: restate.Context, name: string) => { // // - const awakeable = ctx.awakeable(); - const awakeableId = awakeable.id; + const {id, promise} = ctx.awakeable(); // // - await ctx.run(() => triggerTaskAndDeliverId(awakeableId)); + await ctx.run(() => triggerTaskAndDeliverId(id)); // // - const payload = await awakeable.promise; + const payload = await promise; // // // - ctx.resolveAwakeable(awakeableId, "hello"); + ctx.resolveAwakeable(id, "hello"); // // - ctx.rejectAwakeable(awakeableId, "my error reason"); + ctx.rejectAwakeable(id, "my error reason"); // }, }, diff --git a/code_snippets/ts/src/develop/serving.ts b/code_snippets/ts/src/develop/serving.ts index c94bae75..33e6ceba 100644 --- a/code_snippets/ts/src/develop/serving.ts +++ b/code_snippets/ts/src/develop/serving.ts @@ -42,8 +42,7 @@ httpServer.listen(); restate .endpoint() .bind(myService) - .bind(myVirtualObject) - .bind(myWorkflow) + // withClass higlight-line .withIdentityV1("publickeyv1_w7YHemBctH5Ck2nQRQ47iBBqhNHy4FV7t2Usbye2A6f") .listen(); // diff --git a/docs/deploy/cloud.md b/docs/deploy/cloud.md index eb7dfd17..c280c74b 100644 --- a/docs/deploy/cloud.md +++ b/docs/deploy/cloud.md @@ -195,35 +195,9 @@ You must secure access to your service so that only Restate can call it. The easiest way to do this is with our native request identity feature. All requests to your service will be signed with a unique environment-specific private key. You can find the corresponding public key in the environment settings UI, under HTTP Services. -It is safe to include this public key directly in your service code: +It is safe to include this public key directly in your service code. - - -```typescript TypeScript -restate - .endpoint() - .bind(myService) - .withIdentityV1("publickeyv1_8SyC5reu2eTUwGCH4CehFntZAnADvYU6PXZtFyKiTrWy") - .listen(); -``` - -```java Java -RestateHttpEndpointBuilder.builder() - .bind(new MyService()) - .withRequestIdentityVerifier(RequestIdentityVerifier.fromKey("publickeyv1_8SyC5reu2eTUwGCH4CehFntZAnADvYU6PXZtFyKiTrWy")) - .buildAndListen(); -``` - -```go Go -if err := server.NewRestate(). - Bind(restate.Reflect(MyService{})). - WithIdentityV1("publickeyv1_8SyC5reu2eTUwGCH4CehFntZAnADvYU6PXZtFyKiTrWy"). - Start(context.Background(), ":9080"); err != nil { - log.Fatal(err) -} -``` - - +Have a look at the SDK serving documentation to learn how for [TypeScript](/develop/ts/serving#validating-request-identity), [Java, Kotlin](/develop/java/serving#validating-request-identity), [Python](/develop/python/serving#validating-request-identity), and [Go](/develop/go/serving#validating-request-identity). ### Lambda diff --git a/docs/develop/go/state.mdx b/docs/develop/go/state.mdx index d4e8afb7..c7785130 100644 --- a/docs/develop/go/state.mdx +++ b/docs/develop/go/state.mdx @@ -10,7 +10,7 @@ You can store key-value state in Restate. Restate makes sure the state is consistent with the processing of the code execution. **This feature is only available for Virtual Objects:** -- For **Virtual Objects**, the state is isolated per Virtual Object. +- For **Virtual Objects**, the state is isolated per Virtual Object and lives forever (across invocations for that object). You can inspect and edit the K/V state stored in Restate via `psql` and the CLI. diff --git a/docs/develop/java/overview.mdx b/docs/develop/java/overview.mdx index 24a2a656..7cf419bc 100644 --- a/docs/develop/java/overview.mdx +++ b/docs/develop/java/overview.mdx @@ -32,7 +32,7 @@ Handlers can either be part of a [Service](/concepts/services#services-1), a [Vi - Handlers have the `Context` parameter ([JavaDocs](https://javadoc.io/doc/dev.restate/sdk-api/latest/dev/restate/sdk/Context.html)) as the first parameter. Within the handler, you use the `Context` to interact with Restate. The SDK stores the actions you do on the context in the Restate journal to make them durable. - - The input parameter and return type are optional and can be of any type, as long as they are serializable/deserializable using [Jackson Databind](https://github.com/FasterXML/jackson) ([see serialization docs](/develop/java/serialization)). + - The input parameter (at most one) and return type are optional and can be of any type, as long as they are serializable/deserializable using [Jackson Databind](https://github.com/FasterXML/jackson) ([see serialization docs](/develop/java/serialization)). - The service will be reachable under the simple class name `MyService`. You can override it by using the annotation field `name`. - Create an endpoint and bind the service(s) to the Restate endpoint. Listen on the specified port (default `9080`) for connections and requests. @@ -46,7 +46,7 @@ Handlers can either be part of a [Service](/concepts/services#services-1), a [Vi - Handlers have the `Context` parameter ([JavaDocs](https://javadoc.io/doc/dev.restate/sdk-api/latest/dev/restate/sdk/Context.html)) as the first parameter. Within the handler, you use the `Context` to interact with Restate. The SDK stores the actions you do on the context in the Restate journal to make them durable. - - The input parameter and return type are optional and can be of any type, as long as they are serializable/deserializable using [Kotlin serialization](https://kotlinlang.org/docs/serialization.html) ([see serialization docs](/develop/java/serialization)). + - The input parameter (at most one) and return type are optional and can be of any type, as long as they are serializable/deserializable using [Kotlin serialization](https://kotlinlang.org/docs/serialization.html) ([see serialization docs](/develop/java/serialization)). - The service will be reachable under the simple class name `MyService`. You can override it by using the annotation field `name`. - Create an endpoint and bind the service(s) to the Restate endpoint. Listen on the specified port (default `9080`) for connections and requests. diff --git a/docs/develop/java/state.mdx b/docs/develop/java/state.mdx index 521eccc2..e5817221 100644 --- a/docs/develop/java/state.mdx +++ b/docs/develop/java/state.mdx @@ -12,7 +12,7 @@ You can store key-value state in Restate. Restate makes sure the state is consistent with the processing of the code execution. **This feature is only available for Virtual Objects and Workflows:** -- For **Virtual Objects**, the state is isolated per Virtual Object. +- For **Virtual Objects**, the state is isolated per Virtual Object and lives forever (across invocations for that object). - For **Workflows**, you can think of it as if every workflow execution is a new object. So the state is isolated to a single workflow execution. The state can only be mutated by the `run` handler of the workflow. The other handlers can only read the state. diff --git a/docs/develop/python/_category_.json b/docs/develop/python/_category_.json new file mode 100644 index 00000000..17422ef8 --- /dev/null +++ b/docs/develop/python/_category_.json @@ -0,0 +1,7 @@ +{ + "label": "Python SDK", + "position": 4, + "link": { + "type": "generated-index" + } +} diff --git a/docs/develop/python/awakeables.mdx b/docs/develop/python/awakeables.mdx new file mode 100644 index 00000000..22c65744 --- /dev/null +++ b/docs/develop/python/awakeables.mdx @@ -0,0 +1,97 @@ +--- +sidebar_position: 6 +description: "Pause invocations while waiting for an external task completion." +--- + +import Admonition from '@theme/Admonition'; + + +# Awakeables + +Awakeables pause an invocation while waiting for another process to complete a task. +You can use this pattern to let a handler execute a task somewhere else and retrieve the result. +This pattern is also known as the callback (task token) pattern. + +## Creating awakeables + + + + 1. The handler **creates an awakeable**. This contains a String identifier and a Promise. + + ```python + CODE_LOAD::python/src/develop/awakeables.py?1 + ``` + + --- + + 2. The handler **triggers a task/process** and attaches the awakeable ID (e.g. over Kafka, via HTTP,...). + For example, send an HTTP request to a service that executes the task, and attach the ID to the payload. + You use `ctx.run` to avoid re-triggering the task on retries. + + ```python + CODE_LOAD::python/src/develop/awakeables.py?2 + ``` + + --- + + 3. The handler **waits** until the other process has executed the task. + The handler **receives the payload and resumes**. + + ```python + CODE_LOAD::python/src/develop/awakeables.py?3 + ``` + + + +## Completing awakeables + +The external process completes the awakeable by either resolving it with an optional payload or by rejecting it +with its ID and a reason for the failure. This throws [a terminal error](/develop/python/error-handling) in the waiting handler. + + + - Resolving over HTTP with its ID and an optional payload: + + ```shell + curl localhost:8080/restate/awakeables/prom_1PePOqp/resolve + -H 'content-type: application/json' + -d '{"hello": "world"}' + ``` + + + - Rejecting over HTTP with its ID and a reason: + + ```shell + curl localhost:8080/restate/awakeables/prom_1PePOqp/reject + -H 'content-type: text/plain' \ + -d 'Very bad error!' + ``` + + + - Resolving via the SDK with its ID and an optional payload: + + ```python + CODE_LOAD::python/src/develop/awakeables.py#resolve + ``` + + + - Rejecting via the SDK with its ID and a reason: + + ```python + CODE_LOAD::python/src/develop/awakeables.py#reject + ``` + + + + + You can return any payload that can be serialized with _`bytes(json.dumps(obj), "utf-8")`_ and deserialized with _`json.loads(buf)`_. + If not, you need to specify a [custom serializer](/develop/python/serialization). + + + + When running on Function-as-a-Service platforms, such as AWS Lambda, Restate suspends the handler while waiting for the awakeable to be completed. + Since you only pay for the time that the handler is actually running, your don't pay while waiting for the external process to return. + + + + Virtual Objects only process a single invocation at a time, so the Virtual Object will be blocked while waiting on the awakeable to be resolved. + diff --git a/docs/develop/python/durable-timers.mdx b/docs/develop/python/durable-timers.mdx new file mode 100644 index 00000000..9d04d0a2 --- /dev/null +++ b/docs/develop/python/durable-timers.mdx @@ -0,0 +1,42 @@ +--- +sidebar_position: 5 +description: "Let your service sleep for a specified time, guaranteed by Restate." +--- + +import Admonition from '@theme/Admonition'; + +# Scheduling & Timers +The Restate SDK includes durable timers. +You can use these to let handlers sleep for a specified time, or to schedule a handler to be called at a later time. +These timers are resilient to failures and restarts. +Restate stores and keeps track of the timers and triggers them on time, even across failures and restarts. + +## Scheduling Async Tasks + +To schedule a handler to be called at a later time, have a look at the documentation on [delayed calls](/develop/python/service-communication#delayed-calls). + + +## Durable sleep +To sleep in a Restate application for ten seconds, do the following: + +```python +CODE_LOAD::python/src/develop/durable_timers.py +``` + + + Restate suspends the handler while it is sleeping, to free up resources. + This is beneficial for AWS Lambda deployments, since you don't pay for the time the handler is sleeping. + + + + Virtual Objects only process a single invocation at a time, so the Virtual Object will be blocked while sleeping. + + +
+ Clock synchronization Restate Server vs. SDK + + The Restate SDK calculates the wake-up time based on the delay you specify. + The Restate Server then uses this calculated time to wake up the handler. + If the Restate Server and the SDK have different system clocks, the sleep duration might not be accurate. + So make sure that the system clock of the Restate Server and the SDK have the same timezone and are synchronized. +
\ No newline at end of file diff --git a/docs/develop/python/error-handling.mdx b/docs/develop/python/error-handling.mdx new file mode 100644 index 00000000..fcdb071e --- /dev/null +++ b/docs/develop/python/error-handling.mdx @@ -0,0 +1,20 @@ +--- +sidebar_position: 8 +description: "Restate's retry strategy and how to manage it from the SDK." +--- + +# Error Handling + +Restate handles retries for failed invocations. +By default, Restate does infinite retries with an exponential backoff strategy. + +For failures for which you do not want retries, but instead want the invocation to end and the error message +to be propagated back to the caller, you can throw a **terminal error**. + +You can throw a terminal exception with an optional HTTP status code and a message anywhere in your handler, as follows: + +```python +CODE_LOAD::python/src/develop/error_handling.py +``` + +You can catch terminal exceptions. For example, you can catch the terminal exception that comes out of a [call to another service](/develop/python/service-communication#request-response-calls), and build your control flow around it. diff --git a/docs/develop/python/journaling-results.mdx b/docs/develop/python/journaling-results.mdx new file mode 100644 index 00000000..9ec2df41 --- /dev/null +++ b/docs/develop/python/journaling-results.mdx @@ -0,0 +1,35 @@ +--- +sidebar_position: 3 +description: "Learn how to store the results of non-deterministic operations." +--- + +import Admonition from '@theme/Admonition'; +import clsx from "clsx"; + +# Journaling Results + +Restate uses an execution log for replay after failures and suspensions. +This means that non-deterministic results (e.g. database responses, UUID generation) need to be stored in the execution log. +The SDK offers some functionalities to help you with this: +1. **[Journaled actions](/develop/python/journaling-results#journaled-actions)**: Run any block of code and store the result in Restate. Restate replays the result instead of re-executing the block on retries. + +## Journaled actions +You can store the result of a (non-deterministic) operation in the Restate execution log (e.g. database requests, HTTP calls, etc). +Restate replays the result instead of re-executing the operation on retries. + +Here is an example of a database request for which the string response is stored in Restate: +```python +CODE_LOAD::python/src/develop/journaling_results.py#side_effect +``` + +You cannot invoke any methods on the Restate context within a side effect. +This includes actions such as getting state, calling another service, and nesting other journaled actions. + +You can return any payload that can be serialized with _`bytes(json.dumps(obj), "utf-8")`_ and deserialized with _`json.loads(buf)`_. +If not, you need to specify a [custom serializer](/develop/python/serialization). + + +Always immediately await `ctx.run`, before doing any other context calls. +If not, you might bump into non-determinism errors during replay, +because the journaled result can get interleaved with the other context calls in the journal in a non-deterministic way. + diff --git a/docs/develop/python/overview.mdx b/docs/develop/python/overview.mdx new file mode 100644 index 00000000..91f541c8 --- /dev/null +++ b/docs/develop/python/overview.mdx @@ -0,0 +1,77 @@ +--- +sidebar_position: 1 +description: "Get an idea of what a Restate TypeScript service looks like." +--- + +import Admonition from '@theme/Admonition'; + +# Overview + +The Restate Python SDK is open source and can be found on GitHub: ([sdk-python repo](https://github.com/restatedev/sdk-python)). + + +Have a look at the [Python Quickstart](/get_started/quickstart?sdk=python)! + + +Add the `restate_sdk` requirement to your Python project to start developing Restate services. + +The Restate SDK lets you implement handlers. +Handlers can either be part of a [Service](/concepts/services#services-1), a [Virtual Object](/concepts/services#virtual-objects), or a [Workflow](/concepts/services#workflows). Let's have a look at how to define them. + +## Services + +[Services](/concepts/services#services-1) and their handlers are defined as follows: + +```python +CODE_LOAD::python/src/develop/my_service.py +``` + +- Initialize a `Service` and specify the service name (here `MyService`). +- The service definition contains a list of handlers. + Each handler is annotated by `@service_name.handler()`, optionally you can override the name of the handler (here `myHandler`). + This is the name that will be used to call the handler, here you would use `/MyService/myHandler`. +- The function has the Restate Context as its first argument. + Within the handler, you use the `Context` to interact with Restate. + The SDK stores the actions you do on the context in the Restate journal to make them durable. +- The handler input argument (at most one) and return type are optional and can be of any primitive type or a `TypedDict`. + They need to be serializable with `json.dumps` and deserializable with `json.loads`. + If not, you need to specify a [custom serializer](/develop/python/serialization). +- Finally, initialize the app and bind the service(s) to it. + The SDK follows the [ASGI standard](https://asgi.readthedocs.io/en/latest/introduction.html) to serve the app. + The templates and examples use [Hypercorn](https://pypi.org/project/Hypercorn/) as the server. + Have a look at the [serving docs](/develop/python/serving) to learn more. + +## Virtual Objects + +[Virtual Objects](/concepts/services#virtual-objects) and their handlers are defined similarly to services, with the following differences: + +```python +CODE_LOAD::python/src/develop/my_virtual_object.py +``` + +- Initialize a `VirtualObject` and specify the object's name (here `MyVirtualObject`). +- The first argument of each handler must be the `ObjectContext` parameter. +Handlers with the `ObjectContext` parameter can write to the K/V state store. +Only one handler can be active at a time, to ensure consistency. +- If you want to have a handler that executes concurrently to the others and doesn't have write access to the K/V state, add `kind="shared"` as annotation argument and use the `ObjectSharedContext`. +For example, you can use these handlers to read K/V state, or interact with the blocking handler. + +## Workflows + +[Workflows](/concepts/services#workflows) are a special type of Virtual Objects, their definition is similar but with the following differences: + +```python +CODE_LOAD::python/src/develop/my_workflow.py +``` + +- Initialize a `Workflow` and specify its name (here `MyWorkflow`). +- Every workflow implementation needs to have a handler annotated with `@workflow_name.main()` called `run` that implements the workflow logic. +This handler uses the `WorkflowContext` to interact with the SDK. +The `run` handler executes exactly one time per workflow execution/object. +- The other handlers of the workflow are used to interact with the workflow: either query it, or signal it. +They use the `WorkflowSharedContext` to interact with the SDK. +These handlers can run concurrently with the `run` handler and can still be called after the `run` handler has finished. +- [Have a look at the workflow docs to learn more.](/develop/python/workflows) + + +Now that you have a high-level idea of what a Restate service might look like, let's have a look at what the Restate Context allows you to do. diff --git a/docs/develop/python/serialization.mdx b/docs/develop/python/serialization.mdx new file mode 100644 index 00000000..2c286a83 --- /dev/null +++ b/docs/develop/python/serialization.mdx @@ -0,0 +1,30 @@ +--- +sidebar_position: 9 +description: "How to serialize and deserialize data with the Restate SDK." +--- + +# Serialization + +Restate sends data over the network for storing state, journaling actions, awakeables, etc. +There are multiple ways to specify which (de)serializers should be used. + +## Default Serialization + +By default, payloads are serialized with _`bytes(json.dumps(obj), "utf-8")`_ and deserialized with _`json.loads(buf)`_. +If this does not work for your data type, then you need to specify a custom serializer, as shown below. + +## Custom Serialization + +To write a custom serializer, you implement the `Serde` interface. + +For example a custom JSON serializer could look like this: + +```python +CODE_LOAD::python/src/develop/serialization.py#custom +``` + +You then use this serializer in your handlers, as follows: + +```python +CODE_LOAD::python/src/develop/serialization.py#using_custom_serde +``` \ No newline at end of file diff --git a/docs/develop/python/service-communication.mdx b/docs/develop/python/service-communication.mdx new file mode 100644 index 00000000..c7bf883b --- /dev/null +++ b/docs/develop/python/service-communication.mdx @@ -0,0 +1,149 @@ +--- +sidebar_position: 2 +description: "Find out how Restate services can send requests to each other." +--- + +import Admonition from '@theme/Admonition'; + +# Service Communication + +A handler can call another handler and wait for the response (request-response), or it can send a message without waiting for the response. + +## Request-response calls + +Request-response calls are requests where the client waits for the response. + + + + To a Service: + + ```python + CODE_LOAD::python/src/develop/service_communication.py#request_response_service + ``` + + + + + To a Virtual Object: + + ```python + CODE_LOAD::python/src/develop/service_communication.py#request_response_object + ``` + + + + + To a Workflow: + + ```python + CODE_LOAD::python/src/develop/service_communication.py#request_response_workflow + ``` + + + +1. Use `ctx.service_call` to call Services, and `ctx.object_call` to call Virtual Objects. +2. **Specify the handler** you want to call and supply the request. + For Virtual Objects, you also need to supply the key of the Virtual Object you want to call, here `"Mary"`. + For Workflows, you need to supply a workflow ID that is unique per workflow execution. +3. **Await** the call to retrieve the response. + + + These calls are proxied by Restate, and get logged in the journal. + In case of failures, Restate takes care of retries. + + + + Once the `run` handler of the workflow has finished, the other handlers can still be called up to the retention time of the workflow, by default 24 hours. + This can be configured via the [Admin API](/references/admin-api/#tag/service/operation/modify_service) per Workflow definition by setting `workflow_completion_retention`. + + + + Request-response calls to Virtual Objects can lead to deadlocks, in which the Virtual Object remains locked and can't process any more requests. + Some example cases: + - Cross deadlock between Virtual Object A and B: A calls B, and B calls A, both using same keys. + - Cyclical deadlock: A calls B, and B calls C, and C calls A again. + + In this situation, you can use the CLI to unblock the Virtual Object manually by [cancelling invocations](/operate/invocation#cancelling-invocations). + + +## Sending messages + +Handlers can send messages (a.k.a. one-way calls, or fire-and-forget calls), as follows: + + + + To a Service: + + ```python + CODE_LOAD::python/src/develop/service_communication.py#one_way_service + ``` + + + + + + To a Virtual Object: + + ```python + CODE_LOAD::python/src/develop/service_communication.py#one_way_object + ``` + + + + + + To a Workflow: + + ```python + CODE_LOAD::python/src/develop/service_communication.py#one_way_workflow + ``` + + + + + Without Restate, you would usually put a message queue in between the two services, to guarantee the message delivery. + Restate eliminates the need for a message queue because Restate durably logs the request and makes sure it gets executed. + + +## Delayed calls + +A delayed call is a one-way call that gets executed after a specified delay. + +To schedule a delayed call, send a message with a delay parameter, as follows: + + + + To a Service: + + ```python + CODE_LOAD::python/src/develop/service_communication.py#delayed_service + ``` + + + + + To a Virtual Object: + + ```python + CODE_LOAD::python/src/develop/service_communication.py#delayed_object + ``` + + + + + You can also use this functionality to schedule async tasks. + Restate will make sure the task gets executed at the desired time. + + + + Invocations to a Virtual Object are executed serially. + Invocations will execute in the same order in which they arrive at Restate. + For example, assume a handler calls the same Virtual Object twice: + + ```python + CODE_LOAD::python/src/develop/service_communication.py#ordering + ``` + + It is guaranteed that call A will execute before call B. + It is not guaranteed though that call B will be executed immediately after call A, as invocations coming from other handlers/sources, could interleave these two calls. + diff --git a/docs/develop/python/serving.mdx b/docs/develop/python/serving.mdx new file mode 100644 index 00000000..35e5cc55 --- /dev/null +++ b/docs/develop/python/serving.mdx @@ -0,0 +1,33 @@ +--- +sidebar_position: 11 +description: "Set up long-running services or Lambda handlers." +--- + + +# Serving +Restate services can run in a few ways: as an HTTP handler, or as an AWS Lambda handler. +For both, you do the following: + +1. Create the app +2. Bind one or multiple services to it. + +```python example.py +CODE_LOAD::python/src/develop/serving.py#endpoint +``` + +The Python SDK follows the [ASGI](https://asgi.readthedocs.io/en/latest/introduction.html) standard for the serving of the services. +The templates and examples use [Hypercorn](https://pypi.org/project/Hypercorn/) to serve the services. +You can run them as follows: + +```shell +python -m hypercorn --config hypercorn-config.toml -b localhost:9080 example:app +``` + +## Validating request identity + +SDKs can validate that incoming requests come from a particular Restate +instance. You can find out more about request identity in the [Security docs](/operate/security#locking-down-service-access) + +```python +CODE_LOAD::python/src/develop/serving.py#identity +``` diff --git a/docs/develop/python/state.mdx b/docs/develop/python/state.mdx new file mode 100644 index 00000000..3881ef92 --- /dev/null +++ b/docs/develop/python/state.mdx @@ -0,0 +1,62 @@ +--- +sidebar_position: 4 +description: "Explore interacting with Restate's state store." +--- + +import Admonition from '@theme/Admonition'; + +# State +You can store key-value state in Restate. +Restate makes sure the state is consistent with the processing of the code execution. + +**This feature is only available for Virtual Objects and Workflows:** +- For **Virtual Objects**, the state is isolated per Virtual Object and lives forever (across invocations for that object). +- For **Workflows**, you can think of it as if every workflow execution is a new object. So the state is isolated to a single workflow execution. The state can only be mutated by the `run` handler of the workflow. The other handlers can only read the state. + + + You can inspect and edit the K/V state stored in Restate via `psql` and the CLI. + Have a look at the [introspection docs](/operate/introspection#inspecting-application-state) for more information. + + + + You can return any payload that can be serialized with _`bytes(json.dumps(obj), "utf-8")`_ and deserialized with _`json.loads(buf)`_. + If not, you need to specify a [custom serializer](/develop/python/serialization). + + +### Listing state keys +For a single Virtual Object, you can list all the state keys that have entries in the state store via: + +```python +CODE_LOAD::python/src/develop/state.py#statekeys +``` + +### Retrieving state +Use `ctx.get` to retrieve the state for a key: + +```python +CODE_LOAD::python/src/develop/state.py#get +``` + +The return value is `None` if no value was stored. + +### Setting state +Use `ctx.set` to set a new value for a key: + +```python +CODE_LOAD::python/src/develop/state.py#set +``` + +### Clearing state +Use `ctx.clear` to delete the value of a key: + +```python +CODE_LOAD::python/src/develop/state.py#clear +``` + +### Clearing all state + +Delete all the state stored in Restate for a Virtual Object via: + +```python +CODE_LOAD::python/src/develop/state.py#clear_all +``` diff --git a/docs/develop/python/workflows.mdx b/docs/develop/python/workflows.mdx new file mode 100644 index 00000000..7ebde1bb --- /dev/null +++ b/docs/develop/python/workflows.mdx @@ -0,0 +1,136 @@ +--- +sidebar_position: 7 +description: "Find out how Restate services can send requests to each other." +--- + +import clsx from "clsx"; +import Admonition from '@theme/Admonition'; + +# Workflows + +[Workflows](/concepts/services#workflows) are a sequence of steps that gets executed durably. +A workflow can be seen as a special type of [Virtual Object](/concepts/services#virtual-objects) with some special characteristics: + +- Each [workflow definition](/develop/python/overview#workflows) has a `run` handler that is annotated with _`@service_name.main()`_ and implements the workflow logic. +- The `run` handler executes exactly one time for each workflow instance (object / key). +- A [workflow definition](/develop/python/overview#workflows) can implement other handlers that can be called multiple times, and can interact with the workflow. +- Workflows have access to the `WorkflowContext` and `WorkflowSharedContext`, giving them some extra functionality, for example [Durable Promises](/develop/python/workflows#signaling-workflows) to signal workflows. + + + The retention time of a workflow execution is 24 hours after the finishing of the `run` handler. + After this timeout any [K/V state](/develop/python/state) is cleared, the workflow's shared handlers cannot be called anymore, and the [Durable Promises](/develop/python/workflows#signaling-workflows) are discarded. + The retention time can be configured via the [Admin API](/references/admin-api/#tag/service/operation/modify_service) per Workflow definition by setting `workflow_completion_retention`. + + +## Implementing workflows +Have a look at the code example to get a better understanding of how workflows are implemented: + + + + ### The `run` handler + + Every workflow needs a `run` handler. + This handler has access to the same SDK features as Service and Virtual Object handlers. + For example, use [`ctx.run`](/develop/python/journaling-results#journaled-actions) to log intermediate results in Restate and avoid re-execution on replay. + + ```python + CODE_LOAD::python/src/develop/workflows/signup.py?1 + ``` + + --- + + ### Querying workflows + + Similar to Virtual Objects, you can retrieve the [K/V state](/develop/python/state#retrieving-state) of workflows via the other handlers defined in the workflow definition, + For example, here we expose the status of the workflow to external clients. + Every workflow execution can be seen as a new object, so the state is isolated to a single workflow execution. + The state can only be mutated by the `run` handler of the workflow. The other handlers can only read the state. + + ```python + CODE_LOAD::python/src/develop/workflows/signup.py?2 + ``` + + --- + + ### Signaling workflows + + You can use Durable Promises to interact with your running workflows: to let the workflow block until an event occurs, or to send a signal / information into or out of a running workflow. + These promises are durable and distributed, meaning they survive crashes and can be resolved or rejected by any handler in the workflow. + + Do the following: + 1. Create a promise in your one handler that is durable and distributed. For example, here in the `run` handler. + 2. Resolve or reject the promise in any other handler in the workflow. This can be done at most one time. + + ```python + CODE_LOAD::python/src/develop/workflows/signup.py?3 + ``` + + --- + + ### Serving and registering workflows + + You serve workflows in the same way as Services and Virtual Objects: by binding them to an [HTTP endpoint](/develop/python/serving). + Make sure you [register the endpoint](/operate/registration) in Restate before invoking it. + + ```python + CODE_LOAD::python/src/develop/workflows/signup.py?4 + ``` + + + + [Check out some examples of workflows-as-code with Restate on the use case page](/use-cases/workflows). + + +## Submitting workflows from a Restate service + + + [**Submit/query/signal**](/develop/python/service-communication): + Call the workflow handlers in the same way as for Services and Virtual Objects. + This returns the result of the workflow/handler once it has finished. + Use `ctx.workflow_send` to call the handler without waiting for the result. + You can only call the `run` handler (submit) once per workflow ID (here `"someone"`). + + ```python + CODE_LOAD::python/src/develop/workflows/service.py + ``` + + +## Submitting workflows over HTTP + + [**Submit/query/signal**](/invoke/http#request-response-calls-over-http): + Call any handler of the workflow in the same way as for Services and Virtual Objects. + This returns the result of the handler once it has finished. + Add `/send` to the path for one-way calls. + You can only call the `run` handler once per workflow ID (here `"someone"`). + + ```shell + curl localhost:8080/signup/someone/run \ + -H 'content-type: application/json' \ + -d '{"email": "someone@restate.dev"}' + ``` + + + + + [**Attach/peek**](/invoke/http#retrieve-result-of-invocations-and-workflows): + This lets you retrieve the result of a workflow or check if it's finished. + + ```shell + curl localhost:8080/restate/workflow/signup/someone/attach + curl localhost:8080/restate/workflow/signup/someone/output + ``` + + + + +## Inspecting workflows + +Have a look at the [introspection docs](/operate/introspection) on how to inspect workflows. +You can use this to for example: +- [Inspect the progress of a workflow by looking at the invocation journal](/operate/introspection#inspecting-the-invocation-journal) +- [Inspect the K/V state of a workflow](/operate/introspection#inspecting-application-state) + + + + + diff --git a/docs/develop/ts/overview.mdx b/docs/develop/ts/overview.mdx index e88845d7..0a8be116 100644 --- a/docs/develop/ts/overview.mdx +++ b/docs/develop/ts/overview.mdx @@ -13,7 +13,7 @@ The Restate TypeScript SDK is open source and can be found on GitHub: ([sdk-type Have a look at the [TypeScript Quickstart](/get_started/quickstart?sdk=ts)!
-Add the `@restatedev/restate-sdk` dependency to your NodeJS project to start developing Restate services. +Add the `@restatedev/restate-sdk` dependency to your Python project to start developing Restate services. The Restate SDK lets you implement handlers. Handlers can either be part of a [Service](/concepts/services#services-1), a [Virtual Object](/concepts/services#virtual-objects), or a [Workflow](/concepts/services#workflows). Let's have a look at how to define them. @@ -33,7 +33,7 @@ CODE_LOAD::ts/src/develop/my_service.ts The function has the Restate Context as its first argument. Within the handler, you use the `Context` to interact with Restate. The SDK stores the actions you do on the context in the Restate journal to make them durable. -- The handler input parameters and return type are optional and can be of any type, as long as they can be serialized as a Buffer with _`Buffer.from(JSON.stringify(yourObject))`_ and deserialized with _`JSON.parse(result.toString()) as T`_. +- The handler input parameter (at most one) and return type are optional and can be of any type, as long as they can be serialized as a Buffer with _`Buffer.from(JSON.stringify(yourObject))`_ and deserialized with _`JSON.parse(result.toString()) as T`_. - Export the service definition `MyService` so that it can be used by other handlers to call the service. (See [Service Communication docs](/develop/ts/service-communication).) - Finally, create an endpoint and bind the service(s) to the Restate endpoint. Listen on the specified port (default `9080`) for connections and requests. diff --git a/docs/develop/ts/serving.mdx b/docs/develop/ts/serving.mdx index 92456227..7cf96432 100644 --- a/docs/develop/ts/serving.mdx +++ b/docs/develop/ts/serving.mdx @@ -7,11 +7,11 @@ description: "Set up long-running services or Lambda handlers." import Admonition from '@theme/Admonition'; # Serving -Restate services can run in a few ways: as a Node.js HTTP handler, as a AWS +Restate services can run in a few ways: as a Node.js HTTP handler, as an AWS Lambda handler, or on other Javascript runtimes like Bun, Deno and Cloudflare Workers. -## Creating an Node.js HTTP handler +## Creating a Node.js HTTP handler 1. Create the endpoint 2. Bind one or multiple services to it. 3. Listen on the specified port (default `9080`) for connections and requests. @@ -47,7 +47,7 @@ for guidance on how to deploy your services on AWS Lambda. ## Creating a fetch handler Other Javascript runtimes like Deno, Bun and Cloudflare Workers have -build on top of the [Fetch Standard](https://github.com/whatwg/fetch) for +built on top of the [Fetch Standard](https://github.com/whatwg/fetch) for defining HTTP server handlers. To register your service as a fetch handler, use the `/fetch` import component. ```typescript @@ -68,7 +68,7 @@ between the client and the Bun server. Bun services must be discovered with the `--use-http1.1` CLI flag. - Cloudflare Workers do not support end-to-end HTTP2 or bidirectional HTTP1.1, and enabling bidirectional mode will cause invocations to stall and time out. -Services running on Workers must be must be discovered with the `--use-http1.1` +Services running on Workers must be discovered with the `--use-http1.1` CLI flag. ## Validating request identity diff --git a/docs/develop/ts/state.mdx b/docs/develop/ts/state.mdx index 0e03a003..f6771131 100644 --- a/docs/develop/ts/state.mdx +++ b/docs/develop/ts/state.mdx @@ -10,7 +10,7 @@ You can store key-value state in Restate. Restate makes sure the state is consistent with the processing of the code execution. **This feature is only available for Virtual Objects and Workflows:** -- For **Virtual Objects**, the state is isolated per Virtual Object. +- For **Virtual Objects**, the state is isolated per Virtual Object and lives forever (across invocations for that object). - For **Workflows**, you can think of it as if every workflow execution is a new object. So the state is isolated to a single workflow execution. The state can only be mutated by the `run` handler of the workflow. The other handlers can only read the state. diff --git a/docs/develop/ts/workflows.mdx b/docs/develop/ts/workflows.mdx index 25aac240..e9de1a48 100644 --- a/docs/develop/ts/workflows.mdx +++ b/docs/develop/ts/workflows.mdx @@ -69,7 +69,7 @@ Have a look at the code example to get a better understanding of how workflows a ### Serving and registering workflows - You serve workflows in the same way as Services and Virtual Objects: by binding them to an [HTTP endpoint](/develop/ts/serving#creating-an-http-endpoint) or [AWS Lambda handler](/develop/ts/serving#creating-a-lambda-handler). + You serve workflows in the same way as Services and Virtual Objects: by binding them to an [HTTP endpoint](/develop/ts/serving) or [AWS Lambda handler](/develop/ts/serving#creating-a-lambda-handler). Make sure you [register the endpoint or Lambda handler](/operate/registration) in Restate before invoking it. ```ts diff --git a/docs/get_started/tour.mdx b/docs/get_started/tour.mdx index 7b64932b..cf578be0 100644 --- a/docs/get_started/tour.mdx +++ b/docs/get_started/tour.mdx @@ -1033,7 +1033,7 @@ You will fix this later on. Note that the `CheckoutService` is not a Virtual Obj ``` - + Make the `CartObject/checkout` handler call the `CheckoutService/handle` handler. For the request field, you can use a hard-coded string array for now: `["seat2B"]`. @@ -1050,7 +1050,7 @@ You will fix this later on. Note that the `CheckoutService` is not a Virtual Obj Call `CartObject/checkout` as you did [earlier](#request-response-calls-over-http) and have a look at the Restate Server logs again to see what happened: - + ## Durable Execution @@ -1353,7 +1353,7 @@ go run ./part1 ``` - + ```shell python3 -m hypercorn -b localhost:9080 tour/part1/app:app diff --git a/docs/overview.mdx b/docs/overview.mdx index 06bb4c96..b999eb8a 100644 --- a/docs/overview.mdx +++ b/docs/overview.mdx @@ -40,6 +40,10 @@ alt="Kotlin Quickstart"> href="/get_started/quickstart?sdk=go" role="button">Go Quickstart +
), }, @@ -64,6 +68,10 @@ alt="Java Tour"> href="/get_started/tour?sdk=go" role="button">Go Tour +
), }, @@ -109,6 +117,10 @@ alt="Java SDK Documentation"> href="/category/go-sdk" role="button">Go SDK Documentation +
), }, diff --git a/src/plugins/code-loader.js b/src/plugins/code-loader.js index 6d92bcad..072738a3 100644 --- a/src/plugins/code-loader.js +++ b/src/plugins/code-loader.js @@ -1,6 +1,15 @@ const fs = require('fs'); const fetch = require('node-fetch'); +const COMMENT_SYMBOL = { + ts: "//", + java: "//", + kotlin: "//", + python: "#", + go: "//", + proto: "//" +} + const plugin = (options) => { const codeLoadRegex = /^CODE_LOAD::([^#?]+)(?:#([^?]*))?(?:\?(.+))?$/g; @@ -32,7 +41,27 @@ const plugin = (options) => { } } + function extractCommentSymbol(filePath) { + if (filePath.includes(".java")) { + return COMMENT_SYMBOL.java; + } else if (filePath.includes(".kt")) { + return COMMENT_SYMBOL.kotlin; + } else if (filePath.includes(".ts")) { + return COMMENT_SYMBOL.ts; + } else if (filePath.includes(".py")) { + return COMMENT_SYMBOL.python; + } else if (filePath.includes(".go")) { + return COMMENT_SYMBOL.go; + } else if (filePath.includes(".proto")) { + return COMMENT_SYMBOL.proto; + } else { + throw new Error(`language not detected for filepath ${filePath}`) + } + } + function extractAndClean(fileContent, customTag, markNumber, filePath) { + const commentSymbol = extractCommentSymbol(filePath) + const startTag = (customTag) ? `` : ""; const endTag = (customTag) ? `` : ""; if (customTag && !fileContent.includes(startTag)) { @@ -47,17 +76,17 @@ const plugin = (options) => { if(fileContent.includes(startTag) && fileContent.includes(endTag)){ lines = fileContent.split(startTag).pop().split(endTag).shift().split('\n').slice(1,-1) // filter out the forced spotless breaks - .filter(line => !line.includes('// break')); + .filter(line => !line.includes(`${commentSymbol} break`)); } else { lines = fileContent.split('\n') // filter out the forced spotless breaks - .filter(line => !line.includes('// break')); + .filter(line => !line.includes(`${commentSymbol} break`)); } let finalLines = []; if (markNumber) { - const markStartTag = `// `; - const markEndTag = `// `; + const markStartTag = `${commentSymbol} `; + const markEndTag = `${commentSymbol} `; let needToMark = false; lines.forEach(function (line, index) { @@ -76,7 +105,7 @@ const plugin = (options) => { if(!line.includes(' - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/static/img/java.svg b/static/img/java.svg index df8f4d73..7d095933 100644 --- a/static/img/java.svg +++ b/static/img/java.svg @@ -1,6 +1 @@ - - - -java - - \ No newline at end of file + \ No newline at end of file diff --git a/static/img/python.svg b/static/img/python.svg new file mode 100644 index 00000000..70d67802 --- /dev/null +++ b/static/img/python.svg @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + +