diff --git a/.tools/run_jvm_tests.sh b/.tools/run_jvm_tests.sh index c9483da1..5032ec7a 100755 --- a/.tools/run_jvm_tests.sh +++ b/.tools/run_jvm_tests.sh @@ -8,6 +8,8 @@ pushd $PROJECT_ROOT/templates/java-maven && mvn verify && popd || exit pushd $PROJECT_ROOT/templates/kotlin-gradle && ./gradlew check && popd || exit pushd $PROJECT_ROOT/templates/kotlin-gradle-lambda-cdk/lambda && ./gradlew check && popd || exit +pushd $PROJECT_ROOT/basics/basics-java && ./gradlew check && popd || exit + pushd $PROJECT_ROOT/patterns-use-cases/sagas/sagas-java && ./gradlew check && popd || exit pushd $PROJECT_ROOT/tutorials/tour-of-restate-java && ./gradlew check && popd || exit diff --git a/.tools/update_jvm_examples.sh b/.tools/update_jvm_examples.sh index f76e52a2..1b7a4184 100755 --- a/.tools/update_jvm_examples.sh +++ b/.tools/update_jvm_examples.sh @@ -17,6 +17,8 @@ search_and_replace_version_maven $PROJECT_ROOT/templates/java-maven search_and_replace_version_gradle $PROJECT_ROOT/templates/kotlin-gradle search_and_replace_version_gradle $PROJECT_ROOT/templates/kotlin-gradle-lambda-cdk/lambda +search_and_replace_version_gradle $PROJECT_ROOT/basics/basics-java + search_and_replace_version_gradle $PROJECT_ROOT/patterns-use-cases/sagas/sagas-java search_and_replace_version_gradle $PROJECT_ROOT/patterns-use-cases/sagas/sagas-kotlin diff --git a/README.md b/README.md index 7b8a0fce..354a9c11 100644 --- a/README.md +++ b/README.md @@ -43,13 +43,14 @@ challenges. ### Java -| Type | Name / Link | -|------------|-----------------------------------------------------------------| -| Use Cases | [Sagas](patterns-use-cases/sagas/sagas-java) | -| End-to-End | [Food Ordering App](end-to-end-applications/java/food-ordering) | -| Tutorial | [Tour of Restate](tutorials/tour-of-restate-java/) | -| Templates | [Template using Gradle](templates/java-gradle) | -| Templates | [Template using Maven](templates/java-maven) | +| Type | Name / Link | +|------------|----------------------------------------------------------------------------| +| Basics | [Durable Execution, Event-processing, Virtual Objects](basics/basics-java) | +| Use Cases | [Sagas](patterns-use-cases/sagas/sagas-java) | +| End-to-End | [Food Ordering App](end-to-end-applications/java/food-ordering) | +| Tutorial | [Tour of Restate](tutorials/tour-of-restate-java/) | +| Templates | [Template using Gradle](templates/java-gradle) | +| Templates | [Template using Maven](templates/java-maven) | ### Kotlin @@ -81,17 +82,15 @@ Install and run the `restate-server` binary: - Download from https://github.com/restatedev/restate/releases - Install with Homebrew: `brew install restatedev/tap/restate-server` - Install with _npm_: `npm install --global @restatedev/restate-server@latest` - -Or run Restate Server in Docker: - - `docker run --name restate_dev --rm -p 8080:8080 -p 9070:9070 -p 9071:9071 --add-host=host.docker.internal:host-gateway docker.io/restatedev/restate:latest` + - Run in Docker: `docker run --name restate_dev --rm -p 8080:8080 -p 9070:9070 -p 9071:9071 --add-host=host.docker.internal:host-gateway docker.io/restatedev/restate:latest` ### (2) Register the examples at Restate Server -Many examples need to be registered at Restate, so that Restate will proxy their function calls and -do its magic. Once both server and example are running, register the example +The service endpoints need to be registered in Restate, so that Restate will proxy their function calls and +do its magic. Once both server and example are running, register the example: -* Via the [CLI](https://docs.restate.dev/operate/cli): `restate dp reg localhost:9080` +* Via the [CLI](https://docs.restate.dev/operate/cli): `restate deployments register localhost:9080` * Via `curl localhost:9070/deployments -H 'content-type: application/json' -d '{"uri": "http://localhost:9080"}'` **Important** When running Restate with Docker, use `host.docker.internal` instead of `localhost` in the URIs above. diff --git a/basics/basics-java/.gitignore b/basics/basics-java/.gitignore new file mode 100644 index 00000000..53ecbad3 --- /dev/null +++ b/basics/basics-java/.gitignore @@ -0,0 +1,35 @@ +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +# Ignore Gradle project-specific cache directory +.gradle + +# Ignore Gradle build output directory +build + +.idea +*.iml + +# Unignore the gradle wrapper +!gradle/wrapper/gradle-wrapper.jar \ No newline at end of file diff --git a/basics/basics-java/README.md b/basics/basics-java/README.md new file mode 100644 index 00000000..b0b1997b --- /dev/null +++ b/basics/basics-java/README.md @@ -0,0 +1,61 @@ +# Examples of the basic concepts for Restate in TypeScript / JavaScript + +The examples here showcase the most basic building blocks of Restate. **Durable Execution**, +**Durable Promises**, and **Virtual Objects**, and the **Workflows** abstraction built on top +of them. + +The individual example files contain code snippets with comments and a brief descriptions +about how they work and how they can be run. + +### Examples + +* **[Basic Durable Execution:](durable_execution/RoleUpdateService.java):** Running code cleanly + to the end in the presence of failures. Automatic retries and recovery of previously + finished actions. The example applies a series of updates and permission setting changes + to user's profile. + ```shell + ./gradlew -PmainClass=durable_execution.RoleUpdateService run + ``` + +* **[Durable Execution with Compensations](durable_execution_compensation/RoleUpdateService.java):** + 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. + ```shell + ./gradlew -PmainClass=durable_execution_compensation.RoleUpdateService run + ``` + +* **[Virtual Objects](virtual_objects/GreeterObject.java):** Stateful serverless objects + to manage durable consistent state and state-manipulating logic. + ```shell + ./gradlew -PmainClass=virtual_objects.GreeterObject run + ``` + +* **[Kafka Event-processing](events_processing/UserUpdatesService.java):** Processing events to + update various downstream systems with durable event handlers, event-delaying, + in a strict-per-key order. + ```shell + ./gradlew -PmainClass=events_processing.UserUpdatesService run + ``` + +* **[Stateful Event-processing](events_state/ProfileService.java):** Populating state from + events and making is queryable via RPC handlers. + ```shell + ./gradlew -PmainClass=events_state.ProfileService run + ``` + + +### Running the examples + +1. Start Restate Server in a separate shell: `npx restate-server` + +2. Start the relevant example. The commands are listed above for each example. + +3. Register the example at Restate server by calling + `npx restate -y deployment register --force localhost:9080`. + + _Note: the '--force' flag here is to circumvent all checks related to graceful upgrades, because it is only a playground, not a production setup._ + +4. Check the comments in the example for how to interact with the example. + +**NOTE:** When you get an error of the type `{"code":"not_found","message":"Service 'greeter' not found. ...}`, then you forgot step (3) for that example. diff --git a/basics/basics-java/build.gradle.kts b/basics/basics-java/build.gradle.kts new file mode 100644 index 00000000..12b75172 --- /dev/null +++ b/basics/basics-java/build.gradle.kts @@ -0,0 +1,47 @@ +import java.net.URI + +plugins { + java + application +} + +repositories { + mavenCentral() +} + +val restateVersion = "0.9.0" + +dependencies { + annotationProcessor("dev.restate:sdk-api-gen:$restateVersion") + + // Restate SDK + implementation("dev.restate:sdk-api:$restateVersion") + implementation("dev.restate:sdk-http-vertx:$restateVersion") + // To use Jackson to read/write state entries (optional) + implementation("dev.restate:sdk-serde-jackson:$restateVersion") + + // Jackson parameter names + // https://github.com/FasterXML/jackson-modules-java8/tree/2.14/parameter-names + implementation("com.fasterxml.jackson.module:jackson-module-parameter-names:2.16.1") + // Jackson java8 types + implementation("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.16.1") + implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.16.1") + + // Logging (optional) + implementation("org.apache.logging.log4j:log4j-core:2.20.0") +} + +// Set main class +application { + if (project.hasProperty("mainClass")) { + mainClass.set(project.property("mainClass") as String) + } else { + mainClass.set("durable_execution.RoleUpdateService") + } +} + +tasks.withType { + // Using -parameters allows to use Jackson ParameterName feature + // https://github.com/FasterXML/jackson-modules-java8/tree/2.14/parameter-names + options.compilerArgs.add("-parameters") +} \ No newline at end of file diff --git a/basics/basics-java/gradle/wrapper/gradle-wrapper.jar b/basics/basics-java/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..033e24c4 Binary files /dev/null and b/basics/basics-java/gradle/wrapper/gradle-wrapper.jar differ diff --git a/basics/basics-java/gradle/wrapper/gradle-wrapper.properties b/basics/basics-java/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..62f495df --- /dev/null +++ b/basics/basics-java/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/basics/basics-java/gradlew b/basics/basics-java/gradlew new file mode 100755 index 00000000..fcb6fca1 --- /dev/null +++ b/basics/basics-java/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/basics/basics-java/gradlew.bat b/basics/basics-java/gradlew.bat new file mode 100644 index 00000000..6689b85b --- /dev/null +++ b/basics/basics-java/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/basics/basics-java/src/main/java/durable_execution/RoleUpdateService.java b/basics/basics-java/src/main/java/durable_execution/RoleUpdateService.java new file mode 100644 index 00000000..b9138ff0 --- /dev/null +++ b/basics/basics-java/src/main/java/durable_execution/RoleUpdateService.java @@ -0,0 +1,89 @@ +/* + * 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/blob/main/LICENSE + */ + +package durable_execution; + +import dev.restate.sdk.Context; +import dev.restate.sdk.annotation.Handler; +import dev.restate.sdk.annotation.Service; +import dev.restate.sdk.common.CoreSerdes; +import dev.restate.sdk.http.vertx.RestateHttpEndpointBuilder; +import utils.Permission; +import durable_execution.utils.UpdateRequest; + +import static utils.ExampleStubs.applyPermission; +import static utils.ExampleStubs.applyUserRole; + +// 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 +// +@Service +public class RoleUpdateService { + + @Handler + public void applyRoleUpdate(Context ctx, UpdateRequest req) { + + // 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. + boolean success = ctx.run(CoreSerdes.JSON_BOOLEAN, () -> + applyUserRole(req.getUserId(), req.getRole())); + if (!success) { + return; + } + + // 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 permission: req.getPermissions()) { + ctx.run(() -> applyPermission(req.getUserId(), permission)); + } + } + + public static void main(String[] args) { + RestateHttpEndpointBuilder.builder() + .bind(new RoleUpdateService()) + .buildAndListen(); + } +} + +// +// 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. +/* +curl localhost:8080/RoleUpdateService/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" } + ] +}' +*/ \ No newline at end of file diff --git a/basics/basics-java/src/main/java/durable_execution_compensation/RoleUpdateService.java b/basics/basics-java/src/main/java/durable_execution_compensation/RoleUpdateService.java new file mode 100644 index 00000000..2d65375a --- /dev/null +++ b/basics/basics-java/src/main/java/durable_execution_compensation/RoleUpdateService.java @@ -0,0 +1,114 @@ +/* + * 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/blob/main/LICENSE + */ +package durable_execution_compensation; + +import dev.restate.sdk.Context; +import dev.restate.sdk.annotation.Handler; +import dev.restate.sdk.annotation.Service; +import dev.restate.sdk.common.TerminalException; +import dev.restate.sdk.http.vertx.RestateHttpEndpointBuilder; +import dev.restate.sdk.serde.jackson.JacksonSerdes; +import durable_execution.utils.UpdateRequest; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import utils.Permission; +import utils.UserRole; + +import java.util.ArrayList; +import java.util.List; + +import static utils.ExampleStubs.*; + +// This is an example of Durable Execution and compensation logic. +// 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. +// + +@Service +public class RoleUpdateService { + private static final Logger logger = LogManager.getLogger(RoleUpdateService.class); + + @Handler + public void applyRoleUpdate(Context ctx, UpdateRequest update){ + + // 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. + UserRole previousRole = ctx.run(JacksonSerdes.of(UserRole.class), + () -> getCurrentRole(update.getUserId())); + ctx.run(() -> tryApplyUserRole(update.getUserId(), update.getRole())); + + // Apply all permissions in order. + // We collect the previous permission settings to reset if the process fails. + List previousPermissions = new ArrayList<>(); + for (Permission permission : update.getPermissions()) { + try { + Permission previous = ctx.run( + JacksonSerdes.of(Permission.class), + () -> tryApplyPermission(update.getUserId(), permission)); + previousPermissions.add(previous); // remember the previous setting + } catch (TerminalException err) { + rollback(ctx, update.getUserId(), previousRole, previousPermissions); + throw err; + } + } + } + + public static void main(String[] args) { + RestateHttpEndpointBuilder.builder() + .bind(new RoleUpdateService()) + .buildAndListen(); + } + + private void rollback(Context ctx, String userId, UserRole previousRole, List previousPermissions) { + logger.info(">>> !!! ROLLING BACK CHANGES for user ID: " + userId); + + for (Permission prev : previousPermissions) { + ctx.run(() -> tryApplyPermission(userId, prev)); + } + + ctx.run(() -> tryApplyUserRole(userId, previousRole)); + } +} + +// +// 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/RoleUpdateService/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/basics/basics-java/src/main/java/events_processing/UserUpdatesService.java b/basics/basics-java/src/main/java/events_processing/UserUpdatesService.java new file mode 100644 index 00000000..0876f67a --- /dev/null +++ b/basics/basics-java/src/main/java/events_processing/UserUpdatesService.java @@ -0,0 +1,80 @@ +/* + * 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/blob/main/LICENSE + */ + +package events_processing; + +// +// Processing events (from Kafka) to update various downstream systems. +// - Journaling actions in Restate and driving retries from Restate, recovering +// partial progress +// - Preserving the order-per-key, but otherwise allowing high-fanout, because +// processing of events does not block other events. +// - Ability to delay events when the downstream systems are busy, without blocking +// entire partitions. +// + +import dev.restate.sdk.ObjectContext; +import dev.restate.sdk.annotation.Handler; +import dev.restate.sdk.annotation.VirtualObject; +import dev.restate.sdk.common.CoreSerdes; +import dev.restate.sdk.http.vertx.RestateHttpEndpointBuilder; +import events_state.ProfileService; +import utils.UserUpdate; + +import java.time.Duration; + +import static utils.ExampleStubs.*; + +// +// Processing events (from Kafka) to update various downstream systems. +// - Journaling actions in Restate and driving retries from Restate, recovering +// partial progress +// - Preserving the order-per-key, but otherwise allowing high-fanout, because +// processing of events does not block other events. +// - Ability to delay events when the downstream systems are busy, without blocking +// entire partitions. +// + +@VirtualObject +public class UserUpdatesService { + + /* + * uses the Event's key (populated for example from Kafka's key) to route the events to the correct Virtual Object. + * And ensures that events with the same key are processed one after the other. + */ + @Handler + public void updateUserEvent(ObjectContext ctx, UserUpdate update) { + + // event handler is a durably executed function that can use all the features of Restate + String userId = ctx.run(CoreSerdes.JSON_STRING, () -> updateUserProfile(update.getProfile())); + while(userId.equals("NOT_READY")) { + // Delay the processing of the event by sleeping. + // The other events for this Virtual Object / key are queued. + // Events for other keys are processed concurrently. + // The sleep suspends the function (e.g., when running on FaaS). + ctx.sleep(Duration.ofMillis(5000)); + userId = ctx.run(CoreSerdes.JSON_STRING, () -> updateUserProfile(update.getProfile())); + } + + + String finalUserId = userId; + String roleId = ctx.run(CoreSerdes.JSON_STRING, + () -> setUserPermissions(finalUserId, update.getPermissions())); + ctx.run(() -> provisionResources(finalUserId, roleId, update.getResources())); + } + + public static void main(String[] args) { + RestateHttpEndpointBuilder.builder() + .bind(new UserUpdatesService()) + .buildAndListen(); + } + +} diff --git a/basics/basics-java/src/main/java/events_state/ProfileService.java b/basics/basics-java/src/main/java/events_state/ProfileService.java new file mode 100644 index 00000000..9f30cbcf --- /dev/null +++ b/basics/basics-java/src/main/java/events_state/ProfileService.java @@ -0,0 +1,59 @@ +/* + * 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/blob/main/LICENSE + */ + +package events_state; + +import dev.restate.sdk.ObjectContext; +import dev.restate.sdk.annotation.Handler; +import dev.restate.sdk.annotation.VirtualObject; +import dev.restate.sdk.common.CoreSerdes; +import dev.restate.sdk.common.StateKey; +import dev.restate.sdk.http.vertx.RestateHttpEndpointBuilder; +import utils.UserProfile; + +// +// Populate state from events (from Kafka). +// Query the state via simple RPC/HTTP calls. +// +@VirtualObject +public class ProfileService { + + private static final StateKey NAME = + StateKey.of("name", CoreSerdes.JSON_STRING); + + private static final StateKey EMAIL = + StateKey.of("email", CoreSerdes.JSON_STRING); + + @Handler + public void registration(ObjectContext ctx, String name){ + // store in state the user's information as coming from the registration event + ctx.set(NAME, name); + } + + @Handler + public void email(ObjectContext ctx, String email){ + // store in state the user's information as coming from the email event + ctx.set(EMAIL, email); + } + + @Handler + public UserProfile get(ObjectContext ctx){ + var name = ctx.get(NAME).orElse(""); + var email = ctx.get(EMAIL).orElse(""); + return new UserProfile(ctx.key(), name, email); + } + + public static void main(String[] args) { + RestateHttpEndpointBuilder.builder() + .bind(new ProfileService()) + .buildAndListen(); + } +} diff --git a/basics/basics-java/src/main/java/utils/ExampleStubs.java b/basics/basics-java/src/main/java/utils/ExampleStubs.java new file mode 100644 index 00000000..589b41f2 --- /dev/null +++ b/basics/basics-java/src/main/java/utils/ExampleStubs.java @@ -0,0 +1,77 @@ +/* + * 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/blob/main/LICENSE + */ +package utils; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import static utils.FailureStubs.applicationError; +import static utils.FailureStubs.maybeCrash; + +public class ExampleStubs { + + private static final Logger logger = LogManager.getLogger(ExampleStubs.class); + + public static boolean applyUserRole(String userId, UserRole role) { + maybeCrash(0.3); + logger.info(String.format(">>> Applying role %s to user %s", role, userId)); + return true; + } + + public static void applyPermission(String userId, Permission permission) { + maybeCrash(0.2); + logger.info(String.format(">>> Applying permission %s:%s for user %s", + permission.getPermissionKey(), + permission.getSetting(), + userId)); + } + + public static UserRole getCurrentRole(String userId){ + return new UserRole("viewer", "User cannot do much"); + } + + public static void tryApplyUserRole(String userId, UserRole role) { + maybeCrash(0.3); + + if(!role.getRoleKey().equals("viewer")){ + applicationError(0.3, "Role " + role.getRoleKey() + " is not possible for user " + userId); + } + logger.error(String.format(">>> Applying role %s to user %s", role, userId)); + } + + public static Permission tryApplyPermission(String userId, Permission permission){ + maybeCrash(0.3); + + if(!permission.getSetting().equals("blocked")) { + applicationError(0.4, + "Could not apply permission " + permission.getPermissionKey() + + ":" + permission.getSetting() + " for user" + userId + " due to a conflict." + ); + } + logger.info(String.format(">>> Applying permission %s:%s for user %s", + permission.getPermissionKey(), + permission.getSetting(), + userId)); + + return new Permission(permission.getPermissionKey(), "blocked"); + } + + public static String updateUserProfile(String profile) { + return Math.random() < 0.8 ? "NOT_READY" : profile + "-id"; + } + + public static String setUserPermissions(String userId, String permissions) { + return permissions; + } + + public static void provisionResources(String userId, String role, String resources){} + +} diff --git a/basics/basics-java/src/main/java/utils/FailureStubs.java b/basics/basics-java/src/main/java/utils/FailureStubs.java new file mode 100644 index 00000000..2f661a65 --- /dev/null +++ b/basics/basics-java/src/main/java/utils/FailureStubs.java @@ -0,0 +1,43 @@ +/* + * 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/blob/main/LICENSE + */ +package utils; + +import dev.restate.sdk.common.TerminalException; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class FailureStubs { + + private static final Logger logger = LogManager.getLogger(ExampleStubs.class); + + private static final boolean killProcess = System.getenv("CRASH_PROCESS") != null; + + public static void maybeCrash(double probability) { + if (Math.random() < probability) { + logger.error("A failure happened!"); + + if (killProcess) { + logger.error("--- CRASHING THE PROCESS ---"); + System.exit(1); + } else { + throw new RuntimeException("A failure happened!"); + } + } + } + + public static void applicationError(Double probability, String message){ + if(Math.random() < probability){ + logger.error("Action failed: " + message); + throw new TerminalException(message); + } + } + +} diff --git a/basics/basics-java/src/main/java/utils/Permission.java b/basics/basics-java/src/main/java/utils/Permission.java new file mode 100644 index 00000000..c0083b07 --- /dev/null +++ b/basics/basics-java/src/main/java/utils/Permission.java @@ -0,0 +1,38 @@ +/* + * 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/blob/main/LICENSE + */ +package utils; + +public class Permission { + private String permissionKey; + private String setting; + + public Permission(String permissionKey, String setting) { + this.permissionKey = permissionKey; + this.setting = setting; + } + + // Getters and setters + public String getPermissionKey() { + return permissionKey; + } + + public void setPermissionKey(String permissionKey) { + this.permissionKey = permissionKey; + } + + public String getSetting() { + return setting; + } + + public void setSetting(String setting) { + this.setting = setting; + } +} diff --git a/basics/basics-java/src/main/java/utils/UpdateRequest.java b/basics/basics-java/src/main/java/utils/UpdateRequest.java new file mode 100644 index 00000000..b7d003b2 --- /dev/null +++ b/basics/basics-java/src/main/java/utils/UpdateRequest.java @@ -0,0 +1,53 @@ +/* + * 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/blob/main/LICENSE + */ +package durable_execution.utils; + +import utils.Permission; +import utils.UserRole; + +import java.util.List; + +public class UpdateRequest { + private String userId; + private UserRole role; + private List permissions; + + public UpdateRequest(String userId, UserRole role, List permissions) { + this.userId = userId; + this.role = role; + this.permissions = permissions; + } + + // Getters and setters + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + public UserRole getRole() { + return role; + } + + public void setRole(UserRole role) { + this.role = role; + } + + public List getPermissions() { + return permissions; + } + + public void setPermissions(List permissions) { + this.permissions = permissions; + } +} diff --git a/basics/basics-java/src/main/java/utils/UserProfile.java b/basics/basics-java/src/main/java/utils/UserProfile.java new file mode 100644 index 00000000..4e30fbdd --- /dev/null +++ b/basics/basics-java/src/main/java/utils/UserProfile.java @@ -0,0 +1,36 @@ +/* + * 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/blob/main/LICENSE + */ +package utils; + +public class UserProfile { + + private final String id; + private final String name; + private final String email; + + public UserProfile(String id, String name, String email) { + this.id = id; + this.name = name; + this.email = email; + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + public String getEmail() { + return email; + } +} diff --git a/basics/basics-java/src/main/java/utils/UserRole.java b/basics/basics-java/src/main/java/utils/UserRole.java new file mode 100644 index 00000000..c7bb0939 --- /dev/null +++ b/basics/basics-java/src/main/java/utils/UserRole.java @@ -0,0 +1,38 @@ +/* + * 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/blob/main/LICENSE + */ +package utils; + +public class UserRole { + private String roleKey; + private String roleDescription; + + public UserRole(String roleKey, String roleDescription) { + this.roleKey = roleKey; + this.roleDescription = roleDescription; + } + + // Getters and setters + public String getRoleKey() { + return roleKey; + } + + public void setRoleKey(String roleKey) { + this.roleKey = roleKey; + } + + public String getRoleDescription() { + return roleDescription; + } + + public void setRoleDescription(String roleDescription) { + this.roleDescription = roleDescription; + } +} diff --git a/basics/basics-java/src/main/java/utils/UserUpdate.java b/basics/basics-java/src/main/java/utils/UserUpdate.java new file mode 100644 index 00000000..f25a7e94 --- /dev/null +++ b/basics/basics-java/src/main/java/utils/UserUpdate.java @@ -0,0 +1,47 @@ +/* + * 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/blob/main/LICENSE + */ +package utils; + +public class UserUpdate { + private String profile; + private String permissions; + private String resources; + + public UserUpdate(String profile, String permissions, String resource) { + this.profile = profile; + this.permissions = permissions; + this.resources = resource; + } + + public String getProfile() { + return profile; + } + + public void setProfile(String profile) { + this.profile = profile; + } + + public String getPermissions() { + return permissions; + } + + public void setPermissions(String permissions) { + this.permissions = permissions; + } + + public String getResources() { + return resources; + } + + public void setResources(String resources) { + this.resources = resources; + } +} diff --git a/basics/basics-java/src/main/java/virtual_objects/GreeterObject.java b/basics/basics-java/src/main/java/virtual_objects/GreeterObject.java new file mode 100644 index 00000000..ea1e2b06 --- /dev/null +++ b/basics/basics-java/src/main/java/virtual_objects/GreeterObject.java @@ -0,0 +1,71 @@ +/* + * 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/blob/main/LICENSE + */ +package virtual_objects; + +import dev.restate.sdk.ObjectContext; +import dev.restate.sdk.annotation.Handler; +import dev.restate.sdk.annotation.VirtualObject; +import dev.restate.sdk.common.CoreSerdes; +import dev.restate.sdk.common.StateKey; +import dev.restate.sdk.http.vertx.RestateHttpEndpointBuilder; + +// +// 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. +// +// Virtual Objects have their state locally accessible without requiring any database +// connection or lookup. State is exclusive, and atomically committed with the +// method execution. +// +// Virtual Objects are _Stateful Serverless_ constructs. +// + +@VirtualObject +public class GreeterObject { + + private static final StateKey COUNT = + StateKey.of("available-drivers", CoreSerdes.JSON_INT); + + @Handler + public String greet(ObjectContext ctx, String greeting) { + + // Access the state attached to this object (this 'name') + // State access and updates are exclusive and consistent with the invocations + int count = ctx.get(COUNT).orElse(0); + int newCount = count + 1; + ctx.set(COUNT, newCount); + + return String.format( "%s %s, for the %d-th time", greeting, ctx.key(), newCount); + } + + @Handler + public String ungreet(ObjectContext ctx) { + int count = ctx.get(COUNT).orElse(0); + if(count > 0){ + int newCount = count - 1; + ctx.set(COUNT, newCount); + } + + return "Dear " + ctx.key() + ", taking one greeting back: " + count; + } + + public static void main(String[] args) { + RestateHttpEndpointBuilder.builder() + .bind(new GreeterObject()) + .buildAndListen(); + } +} + +// See README for details on how to start and connect to Restate. +// Call this service through HTTP directly the following way: +// Example1: `curl localhost:8080/GreeterObject/mary/greet -H 'content-type: application/json' -d '"Hi"'`; +// Example2: `curl localhost:8080/GreeterObject/barack/greet -H 'content-type: application/json' -d '"Hello"'`; +// Example3: `curl localhost:8080/GreeterObject/mary/ungreet -H 'content-type: application/json' -d ''`; \ No newline at end of file diff --git a/basics/basics-java/src/main/resources/log4j2.properties b/basics/basics-java/src/main/resources/log4j2.properties new file mode 100644 index 00000000..536d1d39 --- /dev/null +++ b/basics/basics-java/src/main/resources/log4j2.properties @@ -0,0 +1,26 @@ +# Set to debug or trace if log4j initialization is failing +status = warn + +# Console appender configuration +appender.console.type = Console +appender.console.name = consoleLogger +appender.console.layout.type = PatternLayout +appender.console.layout.pattern = %d{yyyy-MM-dd HH:mm:ss} %-5p %notEmpty{[%X{restateInvocationTarget}]}%notEmpty{[%X{restateInvocationId}]} %c - %m%n + +# Filter out logging during replay +appender.console.filter.replay.type = ContextMapFilter +appender.console.filter.replay.onMatch = DENY +appender.console.filter.replay.onMismatch = NEUTRAL +appender.console.filter.replay.0.type = KeyValuePair +appender.console.filter.replay.0.key = restateInvocationStatus +appender.console.filter.replay.0.value = REPLAYING + +# Restate logs to debug level +logger.app.name = dev.restate +logger.app.level = info +logger.app.additivity = false +logger.app.appenderRef.console.ref = consoleLogger + +# Root logger +rootLogger.level = info +rootLogger.appenderRef.stdout.ref = consoleLogger \ No newline at end of file diff --git a/basics/basics-typescript/README.md b/basics/basics-typescript/README.md index 94ad335d..65e2a059 100644 --- a/basics/basics-typescript/README.md +++ b/basics/basics-typescript/README.md @@ -1,7 +1,7 @@ # Examples of the basic concepts for Restate in TypeScript / JavaScript -The examples here showcase the most basic building blocks of Restate. **durable execution**, -**durable promises**, and **virtual objects**, and the **workflows** abstraction built on top +The examples here showcase the most basic building blocks of Restate. **Durable Execution**, +**Durable Promises**, and **Virtual Objects**, and the **Workflows** abstraction built on top of them. The individual example files contain code snippets with comments and a brief descriptions @@ -46,7 +46,7 @@ about how they work and how they can be run. 4. Register the example at Restate server by calling `npx restate -y deployment register --force "localhost:9080"`. - _Note: the '--force' flag here is to circumvent all checks relating to graceful upgrades, because it is only a playground, not a production setup._ + _Note: the '--force' flag here is to circumvent all checks related to graceful upgrades, because it is only a playground, not a production setup._ 5. Check the comments in the example for how to interact with the example. diff --git a/basics/basics-typescript/src/1_durable_execution.ts b/basics/basics-typescript/src/1_durable_execution.ts index a8ad9e3f..0a582c59 100644 --- a/basics/basics-typescript/src/1_durable_execution.ts +++ b/basics/basics-typescript/src/1_durable_execution.ts @@ -12,15 +12,18 @@ import * as restate from "@restatedev/restate-sdk"; import { UpdateRequest, applyUserRole, applyPermission } from "./utils/example_stubs"; -// Durable execution ensures code runs to the end, even in the presence of -// failures. Use this for code that updates different systems and needs to -// make sure all updates are applied. + +// 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 journals execution progress. Re-tries use that journal to replay -// previous already completed results, avoiding a repetition of that work and -// ensuring stable deterministic values are used during execution. +// - 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 // @@ -29,17 +32,18 @@ async function applyRoleUpdate(ctx: restate.Context, update: UpdateRequest) { // parameters are durable across retries const { userId, role, permissions } = update; - // apply a change to one system (e.g., DB upsert, API call, ...) - // the side effect persists the result with a consensus method so any - // any later code relies on a deterministic result + // 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. const success = await ctx.run(() => applyUserRole(userId, role)); if (!success) { return; } - // simply loop over the array or permission settings. - // each operation through the Restate context is journaled and recovery restores - // results of previous operations from the journal without re-executing them + // 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 (const permission of permissions) { await ctx.run(() => applyPermission(userId, permission)); } diff --git a/basics/basics-typescript/src/2_durable_execution_compensation.ts b/basics/basics-typescript/src/2_durable_execution_compensation.ts index 2a6e02ac..0a10b284 100644 --- a/basics/basics-typescript/src/2_durable_execution_compensation.ts +++ b/basics/basics-typescript/src/2_durable_execution_compensation.ts @@ -27,13 +27,16 @@ async function applyRoleUpdate(ctx: restate.Context, update: UpdateRequest) { // parameters are durable across retries const { userId, role, permissions: permissions } = update; - // regular failures are re-tries, TerminalErrors are propagated - // nothing applied so far, so we propagate the error directly + // 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. const previousRole = await ctx.run(() => getCurrentRole(userId)); await ctx.run(() => tryApplyUserRole(userId, role)); - // apply all permissions in order - // we collect the previous permission settings to reset if the process fails + // Apply all permissions in order. + // We collect the previous permission settings to reset if the process fails. const previousPermissions: Permission[] = []; for (const permission of permissions) { try { @@ -87,7 +90,7 @@ serve.listen(9080); // and, in case of a terminal error, their reversal. // // This will proceed reliably across the occasional process crash, that we blend in. -// Re-tries will not re-execute previously completed actions again, so each line occurs only once. +// 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 \ diff --git a/basics/basics-typescript/src/3_workflows.ts b/basics/basics-typescript/src/3_workflows.ts index 173a1aa0..de188409 100644 --- a/basics/basics-typescript/src/3_workflows.ts +++ b/basics/basics-typescript/src/3_workflows.ts @@ -85,7 +85,7 @@ export const workflowApi = myWorkflow.api; // "email": "bob@builder.com" // } }' -// or programatically +// or programmatically async function signupUser(userId: string, name: string, email: string) { const rs = restate.clients.connect("http://restate:8080"); const { client, status } = await rs.submitWorkflow(workflowApi, "signup-" + userId, { diff --git a/basics/basics-typescript/src/4_virtual_objects.ts b/basics/basics-typescript/src/4_virtual_objects.ts index d880b0b5..90a85c6a 100644 --- a/basics/basics-typescript/src/4_virtual_objects.ts +++ b/basics/basics-typescript/src/4_virtual_objects.ts @@ -32,8 +32,8 @@ const greeterObject = restate.object({ ) => { const greeting = request?.greeting ?? "Hello"; - // access the state attached to this object (this 'name') - // state access and updates are exclusive and consistent with the invocations + // Access the state attached to this object (this 'name') + // State access and updates are exclusive and consistent with the invocations let count = (await ctx.get("count")) ?? 0; count++; ctx.set("count", count); diff --git a/basics/basics-typescript/src/5_events_processing.ts b/basics/basics-typescript/src/5_events_processing.ts index ef207113..6a16ad10 100644 --- a/basics/basics-typescript/src/5_events_processing.ts +++ b/basics/basics-typescript/src/5_events_processing.ts @@ -21,7 +21,7 @@ import { // // Processing events (from Kafka) to update various downstream systems. -// - Journalling actions in Restate and driving retries from Restate, recovering +// - Journaling actions in Restate and driving retries from Restate, recovering // partial progress // - Preserving the order-per-key, but otherwise allowing high-fanout, because // processing of events does not block other events. @@ -33,7 +33,7 @@ const userUpdates = restate.object({ name: "userUpdates", handlers: { /* - * uses the Event's key (populated for example from Kafka's key) to route the events + * uses the Event's key (populated for example from Kafka's key) to route the events to the correct Virtual Object * and ensure that events with the same key are processed one after the other. */ updateUserEvent: async (ctx: restate.ObjectContext, event: UserUpdate) => { @@ -42,9 +42,10 @@ const userUpdates = restate.object({ // event handler is a durably executed function that can use all the features of Restate let userId = await ctx.run(() => updateUserProfile(profile)); while (userId === NOT_READY) { - // delay the event processing. this delays this event, but only queues events for the same - // key. all other events proceed - no partition blocking. - // the sleep suspends the function (e.g., when running on FaaS) + // Delay the processing of the event by sleeping. + // The other events for this Virtual Object / key are queued. + // Events for other keys are processed concurrently. + // The sleep suspends the function (e.g., when running on FaaS). ctx.sleep(5_000); userId = await ctx.run(() => updateUserProfile(profile)); }