From ec35d36e684c9a8c5814c63d6165fb68fbb29eae Mon Sep 17 00:00:00 2001 From: slinkydeveloper Date: Mon, 11 Dec 2023 12:52:22 +0100 Subject: [PATCH 1/6] Introduce deterministic random --- .../restate/sdk/kotlin/RestateContextImpl.kt | 6 ++++++ .../main/kotlin/dev/restate/sdk/kotlin/api.kt | 15 +++++++++++++++ .../java/dev/restate/sdk/RestateContext.java | 16 ++++++++++++++++ .../dev/restate/sdk/RestateContextImpl.java | 11 +++++++---- .../dev/restate/sdk/common/InvocationId.java | 5 +++++ .../dev/restate/sdk/core/InvocationIdImpl.java | 18 ++++++++++++++++++ 6 files changed, 67 insertions(+), 4 deletions(-) diff --git a/sdk-api-kotlin/src/main/kotlin/dev/restate/sdk/kotlin/RestateContextImpl.kt b/sdk-api-kotlin/src/main/kotlin/dev/restate/sdk/kotlin/RestateContextImpl.kt index 3d52c8e4..7a4766de 100644 --- a/sdk-api-kotlin/src/main/kotlin/dev/restate/sdk/kotlin/RestateContextImpl.kt +++ b/sdk-api-kotlin/src/main/kotlin/dev/restate/sdk/kotlin/RestateContextImpl.kt @@ -9,6 +9,7 @@ package dev.restate.sdk.kotlin import com.google.protobuf.ByteString +import dev.restate.sdk.common.InvocationId import dev.restate.sdk.common.Serde import dev.restate.sdk.common.StateKey import dev.restate.sdk.common.TerminalException @@ -19,6 +20,7 @@ import dev.restate.sdk.common.syscalls.Syscalls import io.grpc.MethodDescriptor import java.lang.Error import kotlin.coroutines.resume +import kotlin.random.Random import kotlin.time.Duration import kotlin.time.toJavaDuration import kotlinx.coroutines.* @@ -184,4 +186,8 @@ internal class RestateContextImpl internal constructor(private val syscalls: Sys override fun awakeableHandle(id: String): AwakeableHandle { return AwakeableHandleImpl(syscalls, id) } + + override fun random(): Random { + return Random(InvocationId.current().toRandomSeed()) + } } diff --git a/sdk-api-kotlin/src/main/kotlin/dev/restate/sdk/kotlin/api.kt b/sdk-api-kotlin/src/main/kotlin/dev/restate/sdk/kotlin/api.kt index 22ca917a..747d588d 100644 --- a/sdk-api-kotlin/src/main/kotlin/dev/restate/sdk/kotlin/api.kt +++ b/sdk-api-kotlin/src/main/kotlin/dev/restate/sdk/kotlin/api.kt @@ -15,6 +15,7 @@ import dev.restate.sdk.common.StateKey import dev.restate.sdk.common.syscalls.Syscalls import io.grpc.MethodDescriptor import java.util.* +import kotlin.random.Random import kotlin.time.Duration /** @@ -198,6 +199,20 @@ sealed interface RestateContext { * @see Awakeable */ fun awakeableHandle(id: String): AwakeableHandle + + /** + * Create a [Random] instance inherently predictable, seeded on the + * [dev.restate.sdk.common.InvocationId], which is not secret. + * + * This instance is useful to generate identifiers, idempotency keys, and for uniform sampling + * from a set of options. If a cryptographically secure value is needed, please generate that + * externally using [sideEffect]. + * + * You MUST NOT use this [Random] instance inside a [sideEffect]. + * + * @return the [Random] instance. + */ + fun random(): Random } /** diff --git a/sdk-api/src/main/java/dev/restate/sdk/RestateContext.java b/sdk-api/src/main/java/dev/restate/sdk/RestateContext.java index b41fa0f6..36e269ec 100644 --- a/sdk-api/src/main/java/dev/restate/sdk/RestateContext.java +++ b/sdk-api/src/main/java/dev/restate/sdk/RestateContext.java @@ -16,6 +16,7 @@ import io.grpc.MethodDescriptor; import java.time.Duration; import java.util.Optional; +import java.util.Random; import javax.annotation.Nonnull; import javax.annotation.concurrent.NotThreadSafe; @@ -207,6 +208,21 @@ default void sideEffect(ThrowingRunnable runnable) throws TerminalException { */ AwakeableHandle awakeableHandle(String id); + /** + * Create a {@link Random} instance inherently predictable, seeded on the {@link InvocationId}, + * which is not secret. + * + *

This instance is useful to generate identifiers, idempotency keys, and for uniform sampling + * from a set of options. If a cryptographically secure value is needed, please generate that + * externally using {@link #sideEffect(Serde, ThrowingSupplier)}. + * + *

You MUST NOT use this {@link Random} instance inside a {@link #sideEffect(Serde, + * ThrowingSupplier)}. + * + * @return the {@link Random} instance. + */ + Random random(); + /** * Build a RestateContext from the underlying {@link Syscalls} object. * diff --git a/sdk-api/src/main/java/dev/restate/sdk/RestateContextImpl.java b/sdk-api/src/main/java/dev/restate/sdk/RestateContextImpl.java index 525c0976..2c311db1 100644 --- a/sdk-api/src/main/java/dev/restate/sdk/RestateContextImpl.java +++ b/sdk-api/src/main/java/dev/restate/sdk/RestateContextImpl.java @@ -9,10 +9,7 @@ package dev.restate.sdk; import com.google.protobuf.ByteString; -import dev.restate.sdk.common.AbortedExecutionException; -import dev.restate.sdk.common.Serde; -import dev.restate.sdk.common.StateKey; -import dev.restate.sdk.common.TerminalException; +import dev.restate.sdk.common.*; import dev.restate.sdk.common.function.ThrowingSupplier; import dev.restate.sdk.common.syscalls.DeferredResult; import dev.restate.sdk.common.syscalls.EnterSideEffectSyscallCallback; @@ -22,6 +19,7 @@ import java.time.Duration; import java.util.Map; import java.util.Optional; +import java.util.Random; import java.util.concurrent.CompletableFuture; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -186,4 +184,9 @@ public void reject(String reason) { } }; } + + @Override + public Random random() { + return new Random(InvocationId.current().toRandomSeed()); + } } diff --git a/sdk-common/src/main/java/dev/restate/sdk/common/InvocationId.java b/sdk-common/src/main/java/dev/restate/sdk/common/InvocationId.java index 83c46206..c99a9d71 100644 --- a/sdk-common/src/main/java/dev/restate/sdk/common/InvocationId.java +++ b/sdk-common/src/main/java/dev/restate/sdk/common/InvocationId.java @@ -28,6 +28,11 @@ static InvocationId current() { return INVOCATION_ID_KEY.get(); } + /** + * @return a seed to be used with {@link java.util.Random}. + */ + long toRandomSeed(); + @Override String toString(); } diff --git a/sdk-core/src/main/java/dev/restate/sdk/core/InvocationIdImpl.java b/sdk-core/src/main/java/dev/restate/sdk/core/InvocationIdImpl.java index be6b4c2d..af291902 100644 --- a/sdk-core/src/main/java/dev/restate/sdk/core/InvocationIdImpl.java +++ b/sdk-core/src/main/java/dev/restate/sdk/core/InvocationIdImpl.java @@ -32,8 +32,26 @@ public int hashCode() { return Objects.hash(id); } + @Override + public long toRandomSeed() { + return stringToSeed(id); + } + @Override public String toString() { return id; } + + // Thanks https://stackoverflow.com/questions/12458383/java-random-numbers-using-a-seed + static long stringToSeed(String s) { + if (s == null) { + return 0; + } + long hash = 0; + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + hash = 31L * hash + c; + } + return hash; + } } From 6ffd9563b0903a4fe84875aa3d9ee1f2710ace33 Mon Sep 17 00:00:00 2001 From: slinkydeveloper Date: Mon, 11 Dec 2023 14:29:14 +0100 Subject: [PATCH 2/6] Improve random seed generation --- .../restate/sdk/core/InvocationIdImpl.java | 42 ++++++++++++------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/sdk-core/src/main/java/dev/restate/sdk/core/InvocationIdImpl.java b/sdk-core/src/main/java/dev/restate/sdk/core/InvocationIdImpl.java index af291902..ac66c5b8 100644 --- a/sdk-core/src/main/java/dev/restate/sdk/core/InvocationIdImpl.java +++ b/sdk-core/src/main/java/dev/restate/sdk/core/InvocationIdImpl.java @@ -9,14 +9,19 @@ package dev.restate.sdk.core; import dev.restate.sdk.common.InvocationId; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.util.Objects; final class InvocationIdImpl implements InvocationId { private final String id; + private Long seed; InvocationIdImpl(String debugId) { this.id = debugId; + this.seed = null; } @Override @@ -34,24 +39,33 @@ public int hashCode() { @Override public long toRandomSeed() { - return stringToSeed(id); + if (seed == null) { + // Hash the seed to SHA-256 to increase entropy + MessageDigest md; + try { + md = MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + byte[] digest = md.digest(id.getBytes(StandardCharsets.UTF_8)); + + // Generate the long + long n = 0; + n |= ((long) (digest[7] & 0xFF) << Byte.SIZE * 7); + n |= ((long) (digest[6] & 0xFF) << Byte.SIZE * 6); + n |= ((long) (digest[5] & 0xFF) << Byte.SIZE * 5); + n |= ((long) (digest[4] & 0xFF) << Byte.SIZE * 4); + n |= ((long) (digest[3] & 0xFF) << Byte.SIZE * 3); + n |= ((digest[2] & 0xFF) << Byte.SIZE * 2); + n |= ((digest[1] & 0xFF) << Byte.SIZE); + n |= (digest[0] & 0xFF); + seed = n; + } + return seed; } @Override public String toString() { return id; } - - // Thanks https://stackoverflow.com/questions/12458383/java-random-numbers-using-a-seed - static long stringToSeed(String s) { - if (s == null) { - return 0; - } - long hash = 0; - for (int i = 0; i < s.length(); i++) { - char c = s.charAt(i); - hash = 31L * hash + c; - } - return hash; - } } From 84fee68b1b24dbf487d91bd607ec293a75f1bb8a Mon Sep 17 00:00:00 2001 From: slinkydeveloper Date: Mon, 11 Dec 2023 15:03:00 +0100 Subject: [PATCH 3/6] Introduce Restate random in sdk-api --- .../java/dev/restate/sdk/RestateContext.java | 15 +----- .../dev/restate/sdk/RestateContextImpl.java | 5 +- .../java/dev/restate/sdk/RestateRandom.java | 47 +++++++++++++++++++ 3 files changed, 51 insertions(+), 16 deletions(-) create mode 100644 sdk-api/src/main/java/dev/restate/sdk/RestateRandom.java diff --git a/sdk-api/src/main/java/dev/restate/sdk/RestateContext.java b/sdk-api/src/main/java/dev/restate/sdk/RestateContext.java index 36e269ec..af97234d 100644 --- a/sdk-api/src/main/java/dev/restate/sdk/RestateContext.java +++ b/sdk-api/src/main/java/dev/restate/sdk/RestateContext.java @@ -16,7 +16,6 @@ import io.grpc.MethodDescriptor; import java.time.Duration; import java.util.Optional; -import java.util.Random; import javax.annotation.Nonnull; import javax.annotation.concurrent.NotThreadSafe; @@ -209,19 +208,9 @@ default void sideEffect(ThrowingRunnable runnable) throws TerminalException { AwakeableHandle awakeableHandle(String id); /** - * Create a {@link Random} instance inherently predictable, seeded on the {@link InvocationId}, - * which is not secret. - * - *

This instance is useful to generate identifiers, idempotency keys, and for uniform sampling - * from a set of options. If a cryptographically secure value is needed, please generate that - * externally using {@link #sideEffect(Serde, ThrowingSupplier)}. - * - *

You MUST NOT use this {@link Random} instance inside a {@link #sideEffect(Serde, - * ThrowingSupplier)}. - * - * @return the {@link Random} instance. + * @see RestateRandom */ - Random random(); + RestateRandom random(); /** * Build a RestateContext from the underlying {@link Syscalls} object. diff --git a/sdk-api/src/main/java/dev/restate/sdk/RestateContextImpl.java b/sdk-api/src/main/java/dev/restate/sdk/RestateContextImpl.java index 2c311db1..d7c1ae0c 100644 --- a/sdk-api/src/main/java/dev/restate/sdk/RestateContextImpl.java +++ b/sdk-api/src/main/java/dev/restate/sdk/RestateContextImpl.java @@ -19,7 +19,6 @@ import java.time.Duration; import java.util.Map; import java.util.Optional; -import java.util.Random; import java.util.concurrent.CompletableFuture; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -186,7 +185,7 @@ public void reject(String reason) { } @Override - public Random random() { - return new Random(InvocationId.current().toRandomSeed()); + public RestateRandom random() { + return new RestateRandom(InvocationId.current().toRandomSeed()); } } diff --git a/sdk-api/src/main/java/dev/restate/sdk/RestateRandom.java b/sdk-api/src/main/java/dev/restate/sdk/RestateRandom.java new file mode 100644 index 00000000..2c13bf0e --- /dev/null +++ b/sdk-api/src/main/java/dev/restate/sdk/RestateRandom.java @@ -0,0 +1,47 @@ +// Copyright (c) 2023 - Restate Software, Inc., Restate GmbH +// +// This file is part of the Restate Java SDK, +// which is released under the MIT license. +// +// You can find a copy of the license in file LICENSE in the root +// directory of this repository or package, or at +// https://github.com/restatedev/sdk-java/blob/main/LICENSE +package dev.restate.sdk; + +import dev.restate.sdk.common.InvocationId; +import dev.restate.sdk.common.Serde; +import dev.restate.sdk.common.function.ThrowingSupplier; +import java.util.Random; +import java.util.UUID; + +/** + * Subclass of {@link Random} inherently predictable, seeded on the {@link InvocationId}, which is + * not secret. + * + *

This instance is useful to generate identifiers, idempotency keys, and for uniform sampling + * from a set of options. If a cryptographically secure value is needed, please generate that + * externally using {@link RestateContext#sideEffect(Serde, ThrowingSupplier)}. + * + *

You MUST NOT use this object inside a {@link RestateContext#sideEffect(Serde, + * ThrowingSupplier)}. + */ +public class RestateRandom extends Random { + RestateRandom(long randomSeed) { + super(randomSeed); + } + + /** + * @throws UnsupportedOperationException You cannot set the seed on RestateRandom + */ + @Override + public synchronized void setSeed(long seed) { + throw new UnsupportedOperationException("You cannot set the seed on RestateRandom"); + } + + /** + * @return a UUID generated using this RNG. + */ + public UUID nextUUID() { + return new UUID(this.nextLong(), this.nextLong()); + } +} From fad18b5f3fe5ad3ee8486ab39eb39251903519a6 Mon Sep 17 00:00:00 2001 From: slinkydeveloper Date: Fri, 22 Dec 2023 13:52:00 +0100 Subject: [PATCH 4/6] Add inside side effect guard --- .../dev/restate/sdk/RestateContextImpl.java | 2 +- .../java/dev/restate/sdk/RestateRandom.java | 23 +++++++++++++++++-- .../restate/sdk/common/syscalls/Syscalls.java | 5 ++++ .../sdk/core/ExecutorSwitchingWrappers.java | 6 +++++ .../sdk/core/InvocationStateMachine.java | 6 ++++- .../dev/restate/sdk/core/SyscallsImpl.java | 5 ++++ 6 files changed, 43 insertions(+), 4 deletions(-) diff --git a/sdk-api/src/main/java/dev/restate/sdk/RestateContextImpl.java b/sdk-api/src/main/java/dev/restate/sdk/RestateContextImpl.java index d7c1ae0c..33b475fa 100644 --- a/sdk-api/src/main/java/dev/restate/sdk/RestateContextImpl.java +++ b/sdk-api/src/main/java/dev/restate/sdk/RestateContextImpl.java @@ -186,6 +186,6 @@ public void reject(String reason) { @Override public RestateRandom random() { - return new RestateRandom(InvocationId.current().toRandomSeed()); + return new RestateRandom(InvocationId.current().toRandomSeed(), this.syscalls); } } diff --git a/sdk-api/src/main/java/dev/restate/sdk/RestateRandom.java b/sdk-api/src/main/java/dev/restate/sdk/RestateRandom.java index 2c13bf0e..7cda1d15 100644 --- a/sdk-api/src/main/java/dev/restate/sdk/RestateRandom.java +++ b/sdk-api/src/main/java/dev/restate/sdk/RestateRandom.java @@ -11,6 +11,7 @@ import dev.restate.sdk.common.InvocationId; import dev.restate.sdk.common.Serde; import dev.restate.sdk.common.function.ThrowingSupplier; +import dev.restate.sdk.common.syscalls.Syscalls; import java.util.Random; import java.util.UUID; @@ -26,8 +27,13 @@ * ThrowingSupplier)}. */ public class RestateRandom extends Random { - RestateRandom(long randomSeed) { + + private final Syscalls syscalls; + private boolean seedInitialized = false; + + RestateRandom(long randomSeed, Syscalls syscalls) { super(randomSeed); + this.syscalls = syscalls; } /** @@ -35,7 +41,11 @@ public class RestateRandom extends Random { */ @Override public synchronized void setSeed(long seed) { - throw new UnsupportedOperationException("You cannot set the seed on RestateRandom"); + if (seedInitialized) { + throw new UnsupportedOperationException("You cannot set the seed on RestateRandom"); + } + super.setSeed(seed); + this.seedInitialized = true; } /** @@ -44,4 +54,13 @@ public synchronized void setSeed(long seed) { public UUID nextUUID() { return new UUID(this.nextLong(), this.nextLong()); } + + @Override + protected int next(int bits) { + if (this.syscalls.isInsideSideEffect()) { + throw new IllegalStateException("You can't use RestateRandom inside a side effect!"); + } + + return super.next(bits); + } } diff --git a/sdk-common/src/main/java/dev/restate/sdk/common/syscalls/Syscalls.java b/sdk-common/src/main/java/dev/restate/sdk/common/syscalls/Syscalls.java index e5e50258..7f0e6b7d 100644 --- a/sdk-common/src/main/java/dev/restate/sdk/common/syscalls/Syscalls.java +++ b/sdk-common/src/main/java/dev/restate/sdk/common/syscalls/Syscalls.java @@ -41,6 +41,11 @@ static Syscalls current() { + Thread.currentThread().getName()); } + /** + * @return true if it's inside a side effect block. + */ + boolean isInsideSideEffect(); + // ----- IO // Note: These are not supposed to be exposed to RestateContext, but they should be used through // gRPC APIs. diff --git a/sdk-core/src/main/java/dev/restate/sdk/core/ExecutorSwitchingWrappers.java b/sdk-core/src/main/java/dev/restate/sdk/core/ExecutorSwitchingWrappers.java index 0a8dd023..d39552a8 100644 --- a/sdk-core/src/main/java/dev/restate/sdk/core/ExecutorSwitchingWrappers.java +++ b/sdk-core/src/main/java/dev/restate/sdk/core/ExecutorSwitchingWrappers.java @@ -193,6 +193,12 @@ public InvocationState getInvocationState() { return syscalls.getInvocationState(); } + @Override + public boolean isInsideSideEffect() { + // We can read this from another thread + return syscalls.isInsideSideEffect(); + } + @Override public void close() { syscallsExecutor.execute(syscalls::close); diff --git a/sdk-core/src/main/java/dev/restate/sdk/core/InvocationStateMachine.java b/sdk-core/src/main/java/dev/restate/sdk/core/InvocationStateMachine.java index 23c6a78a..5cd80b06 100644 --- a/sdk-core/src/main/java/dev/restate/sdk/core/InvocationStateMachine.java +++ b/sdk-core/src/main/java/dev/restate/sdk/core/InvocationStateMachine.java @@ -41,7 +41,7 @@ class InvocationStateMachine implements InvocationFlow.InvocationProcessor { private volatile InvocationState invocationState = InvocationState.WAITING_START; // Used for the side effect guard - private boolean insideSideEffect = false; + private volatile boolean insideSideEffect = false; // Obtained after WAITING_START private ByteString id; @@ -97,6 +97,10 @@ public InvocationState getInvocationState() { return this.invocationState; } + public boolean isInsideSideEffect() { + return this.insideSideEffect; + } + public String getFullyQualifiedMethodName() { return this.fullyQualifiedMethodName; } diff --git a/sdk-core/src/main/java/dev/restate/sdk/core/SyscallsImpl.java b/sdk-core/src/main/java/dev/restate/sdk/core/SyscallsImpl.java index 13911d21..ab543d50 100644 --- a/sdk-core/src/main/java/dev/restate/sdk/core/SyscallsImpl.java +++ b/sdk-core/src/main/java/dev/restate/sdk/core/SyscallsImpl.java @@ -327,6 +327,11 @@ public InvocationState getInvocationState() { return this.stateMachine.getInvocationState(); } + @Override + public boolean isInsideSideEffect() { + return this.stateMachine.isInsideSideEffect(); + } + @Override public void close() { this.stateMachine.end(); From 8422410ef18dcffd2e1680172f701027657ce165 Mon Sep 17 00:00:00 2001 From: slinkydeveloper Date: Fri, 22 Dec 2023 13:52:17 +0100 Subject: [PATCH 5/6] Add test for Random --- .../sdk/kotlin/KotlinCoroutinesTests.kt | 2 +- .../dev/restate/sdk/JavaBlockingTests.java | 5 +- .../test/java/dev/restate/sdk/RandomTest.java | 48 +++++++++++++++++ .../dev/restate/sdk/core/RandomTestSuite.java | 52 +++++++++++++++++++ .../restate/sdk/http/vertx/HttpVertxTests.kt | 31 +++-------- 5 files changed, 112 insertions(+), 26 deletions(-) create mode 100644 sdk-api/src/test/java/dev/restate/sdk/RandomTest.java create mode 100644 sdk-core/src/test/java/dev/restate/sdk/core/RandomTestSuite.java diff --git a/sdk-api-kotlin/src/test/kotlin/dev/restate/sdk/kotlin/KotlinCoroutinesTests.kt b/sdk-api-kotlin/src/test/kotlin/dev/restate/sdk/kotlin/KotlinCoroutinesTests.kt index 66574997..60459ac7 100644 --- a/sdk-api-kotlin/src/test/kotlin/dev/restate/sdk/kotlin/KotlinCoroutinesTests.kt +++ b/sdk-api-kotlin/src/test/kotlin/dev/restate/sdk/kotlin/KotlinCoroutinesTests.kt @@ -20,7 +20,7 @@ class KotlinCoroutinesTests : TestRunner() { return Stream.of(MockSingleThread.INSTANCE, MockMultiThreaded.INSTANCE) } - override fun definitions(): Stream { + public override fun definitions(): Stream { return Stream.of( AwakeableIdTest(), DeferredTest(), diff --git a/sdk-api/src/test/java/dev/restate/sdk/JavaBlockingTests.java b/sdk-api/src/test/java/dev/restate/sdk/JavaBlockingTests.java index 7ff33e5c..4e5f3b9e 100644 --- a/sdk-api/src/test/java/dev/restate/sdk/JavaBlockingTests.java +++ b/sdk-api/src/test/java/dev/restate/sdk/JavaBlockingTests.java @@ -23,7 +23,7 @@ protected Stream executors() { } @Override - protected Stream definitions() { + public Stream definitions() { return Stream.of( new AwakeableIdTest(), new DeferredTest(), @@ -36,6 +36,7 @@ protected Stream definitions() { new StateMachineFailuresTest(), new UserFailuresTest(), new GrpcChannelAdapterTest(), - new RestateCodegenTest()); + new RestateCodegenTest(), + new RandomTest()); } } diff --git a/sdk-api/src/test/java/dev/restate/sdk/RandomTest.java b/sdk-api/src/test/java/dev/restate/sdk/RandomTest.java new file mode 100644 index 00000000..4bc020a0 --- /dev/null +++ b/sdk-api/src/test/java/dev/restate/sdk/RandomTest.java @@ -0,0 +1,48 @@ +// Copyright (c) 2023 - Restate Software, Inc., Restate GmbH +// +// This file is part of the Restate Java SDK, +// which is released under the MIT license. +// +// You can find a copy of the license in file LICENSE in the root +// directory of this repository or package, or at +// https://github.com/restatedev/sdk-java/blob/main/LICENSE +package dev.restate.sdk; + +import dev.restate.sdk.common.TerminalException; +import dev.restate.sdk.core.RandomTestSuite; +import dev.restate.sdk.core.testservices.GreeterRestate; +import dev.restate.sdk.core.testservices.GreetingRequest; +import dev.restate.sdk.core.testservices.GreetingResponse; +import io.grpc.BindableService; + +public class RandomTest extends RandomTestSuite { + + private static class RandomShouldBeDeterministic extends GreeterRestate.GreeterRestateImplBase { + @Override + public GreetingResponse greet(RestateContext context, GreetingRequest request) + throws TerminalException { + return GreetingResponse.newBuilder() + .setMessage(Integer.toString(context.random().nextInt())) + .build(); + } + } + + @Override + protected BindableService randomShouldBeDeterministic() { + return new RandomShouldBeDeterministic(); + } + + private static class RandomInsideSideEffect extends GreeterRestate.GreeterRestateImplBase { + @Override + public GreetingResponse greet(RestateContext context, GreetingRequest request) + throws TerminalException { + context.sideEffect(() -> context.random().nextInt()); + throw new IllegalStateException("This should not unreachable"); + } + } + + @Override + protected BindableService randomInsideSideEffect() { + return new RandomInsideSideEffect(); + } +} diff --git a/sdk-core/src/test/java/dev/restate/sdk/core/RandomTestSuite.java b/sdk-core/src/test/java/dev/restate/sdk/core/RandomTestSuite.java new file mode 100644 index 00000000..1d881199 --- /dev/null +++ b/sdk-core/src/test/java/dev/restate/sdk/core/RandomTestSuite.java @@ -0,0 +1,52 @@ +// Copyright (c) 2023 - Restate Software, Inc., Restate GmbH +// +// This file is part of the Restate Java SDK, +// which is released under the MIT license. +// +// You can find a copy of the license in file LICENSE in the root +// directory of this repository or package, or at +// https://github.com/restatedev/sdk-java/blob/main/LICENSE +package dev.restate.sdk.core; + +import static dev.restate.sdk.core.AssertUtils.*; +import static dev.restate.sdk.core.ProtoUtils.*; +import static dev.restate.sdk.core.TestDefinitions.testInvocation; + +import dev.restate.generated.service.protocol.Protocol; +import dev.restate.sdk.core.TestDefinitions.TestDefinition; +import dev.restate.sdk.core.TestDefinitions.TestSuite; +import dev.restate.sdk.core.testservices.GreeterGrpc; +import dev.restate.sdk.core.testservices.GreetingRequest; +import io.grpc.BindableService; +import java.util.Random; +import java.util.stream.Stream; + +public abstract class RandomTestSuite implements TestSuite { + + protected abstract BindableService randomShouldBeDeterministic(); + + protected abstract BindableService randomInsideSideEffect(); + + @Override + public Stream definitions() { + String debugId = "my-id"; + + int expectedRandomNumber = new Random(new InvocationIdImpl(debugId).toRandomSeed()).nextInt(); + return Stream.of( + testInvocation(this::randomShouldBeDeterministic, GreeterGrpc.getGreetMethod()) + .withInput( + Protocol.StartMessage.newBuilder().setDebugId(debugId).setKnownEntries(1), + inputMessage(GreetingRequest.getDefaultInstance())) + .expectingOutput( + outputMessage(greetingResponse(Integer.toString(expectedRandomNumber))), + END_MESSAGE), + testInvocation(this::randomInsideSideEffect, GreeterGrpc.getGreetMethod()) + .withInput( + Protocol.StartMessage.newBuilder().setDebugId(debugId).setKnownEntries(1), + inputMessage(GreetingRequest.getDefaultInstance())) + .assertingOutput( + containsOnlyExactErrorMessage( + new IllegalStateException( + "You can't use RestateRandom inside a side effect!")))); + } +} diff --git a/sdk-http-vertx/src/test/kotlin/dev/restate/sdk/http/vertx/HttpVertxTests.kt b/sdk-http-vertx/src/test/kotlin/dev/restate/sdk/http/vertx/HttpVertxTests.kt index f12a7d8a..d3d9418e 100644 --- a/sdk-http-vertx/src/test/kotlin/dev/restate/sdk/http/vertx/HttpVertxTests.kt +++ b/sdk-http-vertx/src/test/kotlin/dev/restate/sdk/http/vertx/HttpVertxTests.kt @@ -10,12 +10,14 @@ package dev.restate.sdk.http.vertx import com.google.protobuf.ByteString import dev.restate.generated.sdk.java.Java.SideEffectEntryMessage +import dev.restate.sdk.JavaBlockingTests import dev.restate.sdk.RestateService import dev.restate.sdk.core.ProtoUtils.* import dev.restate.sdk.core.TestDefinitions.* import dev.restate.sdk.core.testservices.GreeterGrpc import dev.restate.sdk.core.testservices.GreetingRequest import dev.restate.sdk.core.testservices.GreetingResponse +import dev.restate.sdk.kotlin.KotlinCoroutinesTests import dev.restate.sdk.kotlin.RestateKtService import io.grpc.stub.StreamObserver import io.vertx.core.Vertx @@ -101,28 +103,11 @@ class HttpVertxTests : dev.restate.sdk.core.TestRunner() { } override fun definitions(): Stream { - return Stream.of( - dev.restate.sdk.AwakeableIdTest(), - dev.restate.sdk.DeferredTest(), - dev.restate.sdk.EagerStateTest(), - dev.restate.sdk.StateTest(), - dev.restate.sdk.InvocationIdTest(), - dev.restate.sdk.OnlyInputAndOutputTest(), - dev.restate.sdk.SideEffectTest(), - dev.restate.sdk.SleepTest(), - dev.restate.sdk.StateMachineFailuresTest(), - dev.restate.sdk.UserFailuresTest(), - dev.restate.sdk.GrpcChannelAdapterTest(), - dev.restate.sdk.kotlin.AwakeableIdTest(), - dev.restate.sdk.kotlin.DeferredTest(), - dev.restate.sdk.kotlin.EagerStateTest(), - dev.restate.sdk.kotlin.StateTest(), - dev.restate.sdk.kotlin.InvocationIdTest(), - dev.restate.sdk.kotlin.OnlyInputAndOutputTest(), - dev.restate.sdk.kotlin.SideEffectTest(), - dev.restate.sdk.kotlin.SleepTest(), - dev.restate.sdk.kotlin.StateMachineFailuresTest(), - dev.restate.sdk.kotlin.UserFailuresTest(), - VertxExecutorsTest()) + return Stream.concat( + Stream.concat( + JavaBlockingTests().definitions(), + KotlinCoroutinesTests().definitions(), + ), + Stream.of(VertxExecutorsTest())) } } From 332895f1e4410990a2cf50633fb016e420991e76 Mon Sep 17 00:00:00 2001 From: slinkydeveloper Date: Fri, 22 Dec 2023 14:13:19 +0100 Subject: [PATCH 6/6] Introduce similar RestateRandom to Kotlin API --- .../restate/sdk/kotlin/RestateContextImpl.kt | 5 +- .../main/kotlin/dev/restate/sdk/kotlin/api.kt | 17 ++++++- .../sdk/kotlin/KotlinCoroutinesTests.kt | 3 +- .../dev/restate/sdk/kotlin/RandomTest.kt | 50 +++++++++++++++++++ .../test/java/dev/restate/sdk/RandomTest.java | 6 +++ .../dev/restate/sdk/core/RandomTestSuite.java | 7 ++- 6 files changed, 81 insertions(+), 7 deletions(-) create mode 100644 sdk-api-kotlin/src/test/kotlin/dev/restate/sdk/kotlin/RandomTest.kt diff --git a/sdk-api-kotlin/src/main/kotlin/dev/restate/sdk/kotlin/RestateContextImpl.kt b/sdk-api-kotlin/src/main/kotlin/dev/restate/sdk/kotlin/RestateContextImpl.kt index 7a4766de..0a8c3527 100644 --- a/sdk-api-kotlin/src/main/kotlin/dev/restate/sdk/kotlin/RestateContextImpl.kt +++ b/sdk-api-kotlin/src/main/kotlin/dev/restate/sdk/kotlin/RestateContextImpl.kt @@ -20,7 +20,6 @@ import dev.restate.sdk.common.syscalls.Syscalls import io.grpc.MethodDescriptor import java.lang.Error import kotlin.coroutines.resume -import kotlin.random.Random import kotlin.time.Duration import kotlin.time.toJavaDuration import kotlinx.coroutines.* @@ -187,7 +186,7 @@ internal class RestateContextImpl internal constructor(private val syscalls: Sys return AwakeableHandleImpl(syscalls, id) } - override fun random(): Random { - return Random(InvocationId.current().toRandomSeed()) + override fun random(): RestateRandom { + return RestateRandom(InvocationId.current().toRandomSeed(), syscalls) } } diff --git a/sdk-api-kotlin/src/main/kotlin/dev/restate/sdk/kotlin/api.kt b/sdk-api-kotlin/src/main/kotlin/dev/restate/sdk/kotlin/api.kt index 747d588d..6ae35beb 100644 --- a/sdk-api-kotlin/src/main/kotlin/dev/restate/sdk/kotlin/api.kt +++ b/sdk-api-kotlin/src/main/kotlin/dev/restate/sdk/kotlin/api.kt @@ -201,7 +201,7 @@ sealed interface RestateContext { fun awakeableHandle(id: String): AwakeableHandle /** - * Create a [Random] instance inherently predictable, seeded on the + * Create a [RestateRandom] instance inherently predictable, seeded on the * [dev.restate.sdk.common.InvocationId], which is not secret. * * This instance is useful to generate identifiers, idempotency keys, and for uniform sampling @@ -212,7 +212,20 @@ sealed interface RestateContext { * * @return the [Random] instance. */ - fun random(): Random + fun random(): RestateRandom +} + +class RestateRandom(seed: Long, private val syscalls: Syscalls) : Random() { + private val r = Random(seed) + + override fun nextBits(bitCount: Int): Int { + check(!syscalls.isInsideSideEffect) { "You can't use RestateRandom inside a side effect!" } + return r.nextBits(bitCount) + } + + fun nextUUID(): UUID { + return UUID(this.nextLong(), this.nextLong()) + } } /** diff --git a/sdk-api-kotlin/src/test/kotlin/dev/restate/sdk/kotlin/KotlinCoroutinesTests.kt b/sdk-api-kotlin/src/test/kotlin/dev/restate/sdk/kotlin/KotlinCoroutinesTests.kt index 60459ac7..a6732a36 100644 --- a/sdk-api-kotlin/src/test/kotlin/dev/restate/sdk/kotlin/KotlinCoroutinesTests.kt +++ b/sdk-api-kotlin/src/test/kotlin/dev/restate/sdk/kotlin/KotlinCoroutinesTests.kt @@ -31,6 +31,7 @@ class KotlinCoroutinesTests : TestRunner() { SideEffectTest(), SleepTest(), StateMachineFailuresTest(), - UserFailuresTest()) + UserFailuresTest(), + RandomTest()) } } diff --git a/sdk-api-kotlin/src/test/kotlin/dev/restate/sdk/kotlin/RandomTest.kt b/sdk-api-kotlin/src/test/kotlin/dev/restate/sdk/kotlin/RandomTest.kt new file mode 100644 index 00000000..106c8c62 --- /dev/null +++ b/sdk-api-kotlin/src/test/kotlin/dev/restate/sdk/kotlin/RandomTest.kt @@ -0,0 +1,50 @@ +// Copyright (c) 2023 - Restate Software, Inc., Restate GmbH +// +// This file is part of the Restate Java SDK, +// which is released under the MIT license. +// +// You can find a copy of the license in file LICENSE in the root +// directory of this repository or package, or at +// https://github.com/restatedev/sdk-java/blob/main/LICENSE +package dev.restate.sdk.kotlin + +import dev.restate.sdk.core.RandomTestSuite +import dev.restate.sdk.core.testservices.GreeterGrpcKt +import dev.restate.sdk.core.testservices.GreetingRequest +import dev.restate.sdk.core.testservices.GreetingResponse +import dev.restate.sdk.core.testservices.greetingResponse +import io.grpc.BindableService +import kotlin.random.Random +import kotlinx.coroutines.Dispatchers + +class RandomTest : RandomTestSuite() { + private class RandomShouldBeDeterministic : + GreeterGrpcKt.GreeterCoroutineImplBase(Dispatchers.Unconfined), RestateKtService { + + override suspend fun greet(request: GreetingRequest): GreetingResponse { + val number = restateContext().random().nextInt() + return greetingResponse { message = number.toString() } + } + } + + override fun randomShouldBeDeterministic(): BindableService { + return RandomShouldBeDeterministic() + } + + private class RandomInsideSideEffect : + GreeterGrpcKt.GreeterCoroutineImplBase(Dispatchers.Unconfined), RestateKtService { + override suspend fun greet(request: GreetingRequest): GreetingResponse { + val ctx = restateContext() + ctx.sideEffect { ctx.random().nextInt() } + throw IllegalStateException("This should not unreachable") + } + } + + override fun randomInsideSideEffect(): BindableService { + return RandomInsideSideEffect() + } + + override fun getExpectedInt(seed: Long): Int { + return Random(seed).nextInt() + } +} diff --git a/sdk-api/src/test/java/dev/restate/sdk/RandomTest.java b/sdk-api/src/test/java/dev/restate/sdk/RandomTest.java index 4bc020a0..fc240f6a 100644 --- a/sdk-api/src/test/java/dev/restate/sdk/RandomTest.java +++ b/sdk-api/src/test/java/dev/restate/sdk/RandomTest.java @@ -14,6 +14,7 @@ import dev.restate.sdk.core.testservices.GreetingRequest; import dev.restate.sdk.core.testservices.GreetingResponse; import io.grpc.BindableService; +import java.util.Random; public class RandomTest extends RandomTestSuite { @@ -45,4 +46,9 @@ public GreetingResponse greet(RestateContext context, GreetingRequest request) protected BindableService randomInsideSideEffect() { return new RandomInsideSideEffect(); } + + @Override + protected int getExpectedInt(long seed) { + return new Random(seed).nextInt(); + } } diff --git a/sdk-core/src/test/java/dev/restate/sdk/core/RandomTestSuite.java b/sdk-core/src/test/java/dev/restate/sdk/core/RandomTestSuite.java index 1d881199..44265328 100644 --- a/sdk-core/src/test/java/dev/restate/sdk/core/RandomTestSuite.java +++ b/sdk-core/src/test/java/dev/restate/sdk/core/RandomTestSuite.java @@ -27,6 +27,8 @@ public abstract class RandomTestSuite implements TestSuite { protected abstract BindableService randomInsideSideEffect(); + protected abstract int getExpectedInt(long seed); + @Override public Stream definitions() { String debugId = "my-id"; @@ -38,7 +40,10 @@ public Stream definitions() { Protocol.StartMessage.newBuilder().setDebugId(debugId).setKnownEntries(1), inputMessage(GreetingRequest.getDefaultInstance())) .expectingOutput( - outputMessage(greetingResponse(Integer.toString(expectedRandomNumber))), + outputMessage( + greetingResponse( + Integer.toString( + getExpectedInt(new InvocationIdImpl(debugId).toRandomSeed())))), END_MESSAGE), testInvocation(this::randomInsideSideEffect, GreeterGrpc.getGreetMethod()) .withInput(