Skip to content

Commit b715d5b

Browse files
Introduce deterministic random (#178)
* Introduce deterministic random * Improve random seed generation * Introduce Restate random in sdk-api * Add inside side effect guard * Add test for Random * Introduce similar RestateRandom to Kotlin API
1 parent baf72af commit b715d5b

File tree

17 files changed

+343
-32
lines changed

17 files changed

+343
-32
lines changed

sdk-api-kotlin/src/main/kotlin/dev/restate/sdk/kotlin/RestateContextImpl.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
package dev.restate.sdk.kotlin
1010

1111
import com.google.protobuf.ByteString
12+
import dev.restate.sdk.common.InvocationId
1213
import dev.restate.sdk.common.Serde
1314
import dev.restate.sdk.common.StateKey
1415
import dev.restate.sdk.common.TerminalException
@@ -184,4 +185,8 @@ internal class RestateContextImpl internal constructor(private val syscalls: Sys
184185
override fun awakeableHandle(id: String): AwakeableHandle {
185186
return AwakeableHandleImpl(syscalls, id)
186187
}
188+
189+
override fun random(): RestateRandom {
190+
return RestateRandom(InvocationId.current().toRandomSeed(), syscalls)
191+
}
187192
}

sdk-api-kotlin/src/main/kotlin/dev/restate/sdk/kotlin/api.kt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import dev.restate.sdk.common.StateKey
1515
import dev.restate.sdk.common.syscalls.Syscalls
1616
import io.grpc.MethodDescriptor
1717
import java.util.*
18+
import kotlin.random.Random
1819
import kotlin.time.Duration
1920

2021
/**
@@ -198,6 +199,33 @@ sealed interface RestateContext {
198199
* @see Awakeable
199200
*/
200201
fun awakeableHandle(id: String): AwakeableHandle
202+
203+
/**
204+
* Create a [RestateRandom] instance inherently predictable, seeded on the
205+
* [dev.restate.sdk.common.InvocationId], which is not secret.
206+
*
207+
* This instance is useful to generate identifiers, idempotency keys, and for uniform sampling
208+
* from a set of options. If a cryptographically secure value is needed, please generate that
209+
* externally using [sideEffect].
210+
*
211+
* You MUST NOT use this [Random] instance inside a [sideEffect].
212+
*
213+
* @return the [Random] instance.
214+
*/
215+
fun random(): RestateRandom
216+
}
217+
218+
class RestateRandom(seed: Long, private val syscalls: Syscalls) : Random() {
219+
private val r = Random(seed)
220+
221+
override fun nextBits(bitCount: Int): Int {
222+
check(!syscalls.isInsideSideEffect) { "You can't use RestateRandom inside a side effect!" }
223+
return r.nextBits(bitCount)
224+
}
225+
226+
fun nextUUID(): UUID {
227+
return UUID(this.nextLong(), this.nextLong())
228+
}
201229
}
202230

203231
/**

sdk-api-kotlin/src/test/kotlin/dev/restate/sdk/kotlin/KotlinCoroutinesTests.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ class KotlinCoroutinesTests : TestRunner() {
2020
return Stream.of(MockSingleThread.INSTANCE, MockMultiThreaded.INSTANCE)
2121
}
2222

23-
override fun definitions(): Stream<TestDefinitions.TestSuite> {
23+
public override fun definitions(): Stream<TestDefinitions.TestSuite> {
2424
return Stream.of(
2525
AwakeableIdTest(),
2626
DeferredTest(),
@@ -31,6 +31,7 @@ class KotlinCoroutinesTests : TestRunner() {
3131
SideEffectTest(),
3232
SleepTest(),
3333
StateMachineFailuresTest(),
34-
UserFailuresTest())
34+
UserFailuresTest(),
35+
RandomTest())
3536
}
3637
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright (c) 2023 - Restate Software, Inc., Restate GmbH
2+
//
3+
// This file is part of the Restate Java SDK,
4+
// which is released under the MIT license.
5+
//
6+
// You can find a copy of the license in file LICENSE in the root
7+
// directory of this repository or package, or at
8+
// https://github.com/restatedev/sdk-java/blob/main/LICENSE
9+
package dev.restate.sdk.kotlin
10+
11+
import dev.restate.sdk.core.RandomTestSuite
12+
import dev.restate.sdk.core.testservices.GreeterGrpcKt
13+
import dev.restate.sdk.core.testservices.GreetingRequest
14+
import dev.restate.sdk.core.testservices.GreetingResponse
15+
import dev.restate.sdk.core.testservices.greetingResponse
16+
import io.grpc.BindableService
17+
import kotlin.random.Random
18+
import kotlinx.coroutines.Dispatchers
19+
20+
class RandomTest : RandomTestSuite() {
21+
private class RandomShouldBeDeterministic :
22+
GreeterGrpcKt.GreeterCoroutineImplBase(Dispatchers.Unconfined), RestateKtService {
23+
24+
override suspend fun greet(request: GreetingRequest): GreetingResponse {
25+
val number = restateContext().random().nextInt()
26+
return greetingResponse { message = number.toString() }
27+
}
28+
}
29+
30+
override fun randomShouldBeDeterministic(): BindableService {
31+
return RandomShouldBeDeterministic()
32+
}
33+
34+
private class RandomInsideSideEffect :
35+
GreeterGrpcKt.GreeterCoroutineImplBase(Dispatchers.Unconfined), RestateKtService {
36+
override suspend fun greet(request: GreetingRequest): GreetingResponse {
37+
val ctx = restateContext()
38+
ctx.sideEffect { ctx.random().nextInt() }
39+
throw IllegalStateException("This should not unreachable")
40+
}
41+
}
42+
43+
override fun randomInsideSideEffect(): BindableService {
44+
return RandomInsideSideEffect()
45+
}
46+
47+
override fun getExpectedInt(seed: Long): Int {
48+
return Random(seed).nextInt()
49+
}
50+
}

sdk-api/src/main/java/dev/restate/sdk/RestateContext.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,11 @@ default void sideEffect(ThrowingRunnable runnable) throws TerminalException {
207207
*/
208208
AwakeableHandle awakeableHandle(String id);
209209

210+
/**
211+
* @see RestateRandom
212+
*/
213+
RestateRandom random();
214+
210215
/**
211216
* Build a RestateContext from the underlying {@link Syscalls} object.
212217
*

sdk-api/src/main/java/dev/restate/sdk/RestateContextImpl.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,7 @@
99
package dev.restate.sdk;
1010

1111
import com.google.protobuf.ByteString;
12-
import dev.restate.sdk.common.AbortedExecutionException;
13-
import dev.restate.sdk.common.Serde;
14-
import dev.restate.sdk.common.StateKey;
15-
import dev.restate.sdk.common.TerminalException;
12+
import dev.restate.sdk.common.*;
1613
import dev.restate.sdk.common.function.ThrowingSupplier;
1714
import dev.restate.sdk.common.syscalls.DeferredResult;
1815
import dev.restate.sdk.common.syscalls.EnterSideEffectSyscallCallback;
@@ -186,4 +183,9 @@ public void reject(String reason) {
186183
}
187184
};
188185
}
186+
187+
@Override
188+
public RestateRandom random() {
189+
return new RestateRandom(InvocationId.current().toRandomSeed(), this.syscalls);
190+
}
189191
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Copyright (c) 2023 - Restate Software, Inc., Restate GmbH
2+
//
3+
// This file is part of the Restate Java SDK,
4+
// which is released under the MIT license.
5+
//
6+
// You can find a copy of the license in file LICENSE in the root
7+
// directory of this repository or package, or at
8+
// https://github.com/restatedev/sdk-java/blob/main/LICENSE
9+
package dev.restate.sdk;
10+
11+
import dev.restate.sdk.common.InvocationId;
12+
import dev.restate.sdk.common.Serde;
13+
import dev.restate.sdk.common.function.ThrowingSupplier;
14+
import dev.restate.sdk.common.syscalls.Syscalls;
15+
import java.util.Random;
16+
import java.util.UUID;
17+
18+
/**
19+
* Subclass of {@link Random} inherently predictable, seeded on the {@link InvocationId}, which is
20+
* not secret.
21+
*
22+
* <p>This instance is useful to generate identifiers, idempotency keys, and for uniform sampling
23+
* from a set of options. If a cryptographically secure value is needed, please generate that
24+
* externally using {@link RestateContext#sideEffect(Serde, ThrowingSupplier)}.
25+
*
26+
* <p>You MUST NOT use this object inside a {@link RestateContext#sideEffect(Serde,
27+
* ThrowingSupplier)}.
28+
*/
29+
public class RestateRandom extends Random {
30+
31+
private final Syscalls syscalls;
32+
private boolean seedInitialized = false;
33+
34+
RestateRandom(long randomSeed, Syscalls syscalls) {
35+
super(randomSeed);
36+
this.syscalls = syscalls;
37+
}
38+
39+
/**
40+
* @throws UnsupportedOperationException You cannot set the seed on RestateRandom
41+
*/
42+
@Override
43+
public synchronized void setSeed(long seed) {
44+
if (seedInitialized) {
45+
throw new UnsupportedOperationException("You cannot set the seed on RestateRandom");
46+
}
47+
super.setSeed(seed);
48+
this.seedInitialized = true;
49+
}
50+
51+
/**
52+
* @return a UUID generated using this RNG.
53+
*/
54+
public UUID nextUUID() {
55+
return new UUID(this.nextLong(), this.nextLong());
56+
}
57+
58+
@Override
59+
protected int next(int bits) {
60+
if (this.syscalls.isInsideSideEffect()) {
61+
throw new IllegalStateException("You can't use RestateRandom inside a side effect!");
62+
}
63+
64+
return super.next(bits);
65+
}
66+
}

sdk-api/src/test/java/dev/restate/sdk/JavaBlockingTests.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ protected Stream<TestExecutor> executors() {
2323
}
2424

2525
@Override
26-
protected Stream<TestSuite> definitions() {
26+
public Stream<TestSuite> definitions() {
2727
return Stream.of(
2828
new AwakeableIdTest(),
2929
new DeferredTest(),
@@ -36,6 +36,7 @@ protected Stream<TestSuite> definitions() {
3636
new StateMachineFailuresTest(),
3737
new UserFailuresTest(),
3838
new GrpcChannelAdapterTest(),
39-
new RestateCodegenTest());
39+
new RestateCodegenTest(),
40+
new RandomTest());
4041
}
4142
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright (c) 2023 - Restate Software, Inc., Restate GmbH
2+
//
3+
// This file is part of the Restate Java SDK,
4+
// which is released under the MIT license.
5+
//
6+
// You can find a copy of the license in file LICENSE in the root
7+
// directory of this repository or package, or at
8+
// https://github.com/restatedev/sdk-java/blob/main/LICENSE
9+
package dev.restate.sdk;
10+
11+
import dev.restate.sdk.common.TerminalException;
12+
import dev.restate.sdk.core.RandomTestSuite;
13+
import dev.restate.sdk.core.testservices.GreeterRestate;
14+
import dev.restate.sdk.core.testservices.GreetingRequest;
15+
import dev.restate.sdk.core.testservices.GreetingResponse;
16+
import io.grpc.BindableService;
17+
import java.util.Random;
18+
19+
public class RandomTest extends RandomTestSuite {
20+
21+
private static class RandomShouldBeDeterministic extends GreeterRestate.GreeterRestateImplBase {
22+
@Override
23+
public GreetingResponse greet(RestateContext context, GreetingRequest request)
24+
throws TerminalException {
25+
return GreetingResponse.newBuilder()
26+
.setMessage(Integer.toString(context.random().nextInt()))
27+
.build();
28+
}
29+
}
30+
31+
@Override
32+
protected BindableService randomShouldBeDeterministic() {
33+
return new RandomShouldBeDeterministic();
34+
}
35+
36+
private static class RandomInsideSideEffect extends GreeterRestate.GreeterRestateImplBase {
37+
@Override
38+
public GreetingResponse greet(RestateContext context, GreetingRequest request)
39+
throws TerminalException {
40+
context.sideEffect(() -> context.random().nextInt());
41+
throw new IllegalStateException("This should not unreachable");
42+
}
43+
}
44+
45+
@Override
46+
protected BindableService randomInsideSideEffect() {
47+
return new RandomInsideSideEffect();
48+
}
49+
50+
@Override
51+
protected int getExpectedInt(long seed) {
52+
return new Random(seed).nextInt();
53+
}
54+
}

sdk-common/src/main/java/dev/restate/sdk/common/InvocationId.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ static InvocationId current() {
2828
return INVOCATION_ID_KEY.get();
2929
}
3030

31+
/**
32+
* @return a seed to be used with {@link java.util.Random}.
33+
*/
34+
long toRandomSeed();
35+
3136
@Override
3237
String toString();
3338
}

0 commit comments

Comments
 (0)