Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce deterministic random #178

Merged
merged 6 commits into from
Dec 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -184,4 +185,8 @@ internal class RestateContextImpl internal constructor(private val syscalls: Sys
override fun awakeableHandle(id: String): AwakeableHandle {
return AwakeableHandleImpl(syscalls, id)
}

override fun random(): RestateRandom {
return RestateRandom(InvocationId.current().toRandomSeed(), syscalls)
}
}
28 changes: 28 additions & 0 deletions sdk-api-kotlin/src/main/kotlin/dev/restate/sdk/kotlin/api.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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

/**
Expand Down Expand Up @@ -198,6 +199,33 @@ sealed interface RestateContext {
* @see Awakeable
*/
fun awakeableHandle(id: String): AwakeableHandle

/**
* 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
* 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(): 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())
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class KotlinCoroutinesTests : TestRunner() {
return Stream.of(MockSingleThread.INSTANCE, MockMultiThreaded.INSTANCE)
}

override fun definitions(): Stream<TestDefinitions.TestSuite> {
public override fun definitions(): Stream<TestDefinitions.TestSuite> {
return Stream.of(
AwakeableIdTest(),
DeferredTest(),
Expand All @@ -31,6 +31,7 @@ class KotlinCoroutinesTests : TestRunner() {
SideEffectTest(),
SleepTest(),
StateMachineFailuresTest(),
UserFailuresTest())
UserFailuresTest(),
RandomTest())
}
}
Original file line number Diff line number Diff line change
@@ -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()
}
}
5 changes: 5 additions & 0 deletions sdk-api/src/main/java/dev/restate/sdk/RestateContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,11 @@ default void sideEffect(ThrowingRunnable runnable) throws TerminalException {
*/
AwakeableHandle awakeableHandle(String id);

/**
* @see RestateRandom
*/
RestateRandom random();

/**
* Build a RestateContext from the underlying {@link Syscalls} object.
*
Expand Down
10 changes: 6 additions & 4 deletions sdk-api/src/main/java/dev/restate/sdk/RestateContextImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -186,4 +183,9 @@ public void reject(String reason) {
}
};
}

@Override
public RestateRandom random() {
return new RestateRandom(InvocationId.current().toRandomSeed(), this.syscalls);
}
}
66 changes: 66 additions & 0 deletions sdk-api/src/main/java/dev/restate/sdk/RestateRandom.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// 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 dev.restate.sdk.common.syscalls.Syscalls;
import java.util.Random;
import java.util.UUID;

/**
* Subclass of {@link Random} inherently predictable, seeded on the {@link InvocationId}, which is
* not secret.
*
* <p>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)}.
*
* <p>You MUST NOT use this object inside a {@link RestateContext#sideEffect(Serde,
* ThrowingSupplier)}.
*/
public class RestateRandom extends Random {

private final Syscalls syscalls;
private boolean seedInitialized = false;

RestateRandom(long randomSeed, Syscalls syscalls) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Should this be just BooleanSupplier ?

Copy link
Contributor Author

@slinkydeveloper slinkydeveloper Dec 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer to use Syscalls here, it makes a bit more clear this code.

super(randomSeed);
this.syscalls = syscalls;
}

/**
* @throws UnsupportedOperationException You cannot set the seed on RestateRandom
*/
@Override
public synchronized void setSeed(long seed) {
if (seedInitialized) {
throw new UnsupportedOperationException("You cannot set the seed on RestateRandom");
}
super.setSeed(seed);
this.seedInitialized = true;
}

/**
* @return a UUID generated using this RNG.
*/
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);
}
}
5 changes: 3 additions & 2 deletions sdk-api/src/test/java/dev/restate/sdk/JavaBlockingTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ protected Stream<TestExecutor> executors() {
}

@Override
protected Stream<TestSuite> definitions() {
public Stream<TestSuite> definitions() {
return Stream.of(
new AwakeableIdTest(),
new DeferredTest(),
Expand All @@ -36,6 +36,7 @@ protected Stream<TestSuite> definitions() {
new StateMachineFailuresTest(),
new UserFailuresTest(),
new GrpcChannelAdapterTest(),
new RestateCodegenTest());
new RestateCodegenTest(),
new RandomTest());
}
}
54 changes: 54 additions & 0 deletions sdk-api/src/test/java/dev/restate/sdk/RandomTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// 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;
import java.util.Random;

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();
}

@Override
protected int getExpectedInt(long seed) {
return new Random(seed).nextInt();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
32 changes: 32 additions & 0 deletions sdk-core/src/main/java/dev/restate/sdk/core/InvocationIdImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -32,6 +37,33 @@ public int hashCode() {
return Objects.hash(id);
}

@Override
public long toRandomSeed() {
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;
Expand Down
Loading
Loading