Skip to content
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
73 changes: 73 additions & 0 deletions .junie/guidelines.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
Project-specific development guidelines

Scope
- This document captures only information that is particular to this repository (sdk-java). It assumes familiarity with Gradle, JUnit 5, Docker, and multi-module builds.

Build and configuration
- JDK/Toolchains
- Java toolchain: 17 for compile/run (auto-provisioned via org.gradle.toolchains.foojay-resolver-convention in settings.gradle.kts). CI also validates with Java 21.
- You do NOT need a locally installed JDK 17 if your Gradle has toolchains enabled; Gradle will download it.
- Gradle wrapper
- Always use the provided wrapper: ./gradlew ...
- Wrapper version: see gradle/wrapper/gradle-wrapper.properties (8.x). CI uses the wrapper as well.
- Root build highlights (build.gradle.kts)
- Versioning via Gradle Version Catalog: gradle/libs.versions.toml. The SDK version is libs.versions.restate.
- Spotless is enforced across all projects (Google Java Format for Java, ktfmt for Kotlin, license headers from config/license-header). The check task depends on checkLicense from the license-report plugin.
- Dokka is applied to most subprojects for Kotlin docs; aggregated Javadocs live in :sdk-aggregated-javadocs.
- Publishing is wired via io.github.gradle-nexus.publish-plugin. Sonatype credentials must be provided as MAVEN_CENTRAL_USERNAME and MAVEN_CENTRAL_TOKEN environment variables when publishing.
- Subprojects layout
- Core libraries: common, client, client-kotlin, sdk-common, sdk-core, sdk-serde-*, sdk-request-identity, sdk-api*, sdk-http-vertx, sdk-lambda, sdk-spring-boot*, starters, meta modules (sdk-*-http/lambda), examples, test-services.
- Java/Kotlin conventions are centralized in buildSrc:
- java-conventions.gradle.kts: toolchain 17, JUnit Platform, Spotless with googleJavaFormat.
- kotlin-conventions.gradle.kts: toolchain 17, JUnit Platform, Spotless with ktfmt, license headers.

Testing
- Frameworks and dependencies
- JUnit 5 Platform is enabled globally (tasks.withType<Test> { useJUnitPlatform() }).
- AssertJ is available broadly via the version catalog and commonly applied in modules.
- Some integration tests use Testcontainers and the Restate runtime. The sdk-testing module provides a JUnit 5 extension and utilities (RestateTest, RestateRunner) to spin up Restate and auto-register in-process services.
- Running tests
- All modules: ./gradlew test
- Single module: ./gradlew :common:test (replace :common with the desired module)
- Single class or method: ./gradlew :common:test --tests 'dev.restate.common.SomethingTest' or --tests 'dev.restate.common.SomethingTest.methodName'
- CI pulls the Restate Docker image explicitly and tests on Java 17 and 21 (see .github/workflows/tests.yml). Locally, if you run integration tests that leverage @RestateTest, ensure Docker is running; Testcontainers will pull the required image on demand.
- Restate integration testing (sdk-testing)
- Annotate your JUnit 5 test class with @RestateTest to bootstrap a Restate runtime in a container and register your services.
- Example sketch:
@RestateTest(containerImage = "ghcr.io/restatedev/restate:main")
class CounterTest { /* fields annotated with @BindService, inject @RestateClient, etc. */ }
- The default image is docker.io/restatedev/restate:latest; CI uses ghcr.io/restatedev/restate:main. You can override via containerImage or add env via environment() in the annotation.
- Under the hood, RestateRunner uses Testcontainers and opens ports 8080 (ingress) and 9070 (admin). Docker must be available.
- Adding tests
- Java tests: place under src/test/java and name *Test.java (JUnit 5). Kotlin tests under src/test/kotlin.
- Dependencies are already configured in most modules (e.g., common includes testImplementation(libs.junit.jupiter) and testImplementation(libs.assertj)). If adding tests to a module without these, add them in that module's build.gradle.kts.
- For Restate-based tests, add a dependency on sdk-testing if not already present and use the annotations provided in dev.restate.sdk.testing.*.
- Example test run (verified locally)
- A simple JUnit 5 test was created temporarily in :common and executed via:
./gradlew :common:test --no-daemon
- The build succeeded. The temporary test file was then removed to avoid polluting the repo.

Development and debugging tips
- Formatting and license headers
- Run formatting checks: ./gradlew spotlessCheck
- Apply formatting and headers: ./gradlew spotlessApply
- Any newly added source files must include the license header from config/license-header (Spotless can apply it).
- Dependency and license compliance
- The check task depends on checkLicense, which uses allowed-licenses.json and normalizer config under config/. If you add new dependencies, ensure license reporting stays green.
- Version management
- Add/upgrade dependencies in gradle/libs.versions.toml. Prefer using the version catalog aliases (libs.*) in module build files. The overall SDK version is controlled by versions.restate.
- Docs
- Aggregated Javadoc: ./gradlew :sdk-aggregated-javadocs:javadoc
- Kotlin docs: ./gradlew dokkaHtmlMultiModule
- Docker-based tests
- If integration tests fail locally while CI is green, verify Docker daemon availability and that the Restate image is accessible. You may pre-pull CI's image: docker pull ghcr.io/restatedev/restate:main
- IDE setup
- Use Gradle import. The toolchain resolver will fetch JDK 17 automatically. Ensure your IDE respects the Gradle JVM and uses language level 17 for compilation.

Troubleshooting
- Classpath conflicts when running Dokka: The root buildscript pins Jackson modules to 2.17.1 specifically to avoid Dokka bringing unshaded variants that break other plugins. Keep these overrides if upgrading Dokka.
- If tests that rely on Testcontainers hang on startup, check network/firewall settings and whether Testcontainers can communicate with Docker. You can enable more verbose logs with TESTCONTAINERS_LOG_LEVEL=DEBUG.

Housekeeping
- Do not commit temporary tests used for local verification; keep the tree clean.
- Before publishing or opening PRs, run: ./gradlew clean build
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ package dev.restate.sdk.kotlin.endpoint

import dev.restate.sdk.endpoint.Endpoint
import dev.restate.sdk.endpoint.definition.HandlerDefinition
import dev.restate.sdk.endpoint.definition.InvocationRetryPolicy
import dev.restate.sdk.endpoint.definition.ServiceDefinition
import kotlin.time.Duration
import kotlin.time.toJavaDuration
Expand Down Expand Up @@ -150,6 +151,22 @@ var ServiceDefinition.Configurator.ingressPrivate: Boolean?
this.ingressPrivate(value)
}

/**
* Retry policy used by Restate when invoking this service.
*
* <p><b>NOTE:</b> You can set this field only if you register this service against
* restate-server >= 1.5, otherwise the service discovery will fail.
*
* @see InvocationRetryPolicy
*/
var ServiceDefinition.Configurator.invocationRetryPolicy: InvocationRetryPolicy?
get() {
return this.invocationRetryPolicy()
}
set(value) {
this.invocationRetryPolicy(value)
}

/**
* Set the acceptable content type when ingesting HTTP requests. Wildcards can be used, e.g.
* `application/*` or `*/*`.
Expand Down Expand Up @@ -298,3 +315,82 @@ var HandlerDefinition.Configurator.enableLazyState: Boolean?
set(value) {
this.enableLazyState(value)
}

/**
* Retry policy used by Restate when invoking this handler.
*
* <p><b>NOTE:</b> You can set this field only if you register this service against
* restate-server >= 1.5, otherwise the service discovery will fail.
*
* @see InvocationRetryPolicy
*/
var HandlerDefinition.Configurator.invocationRetryPolicy: InvocationRetryPolicy?
get() {
return this.invocationRetryPolicy()
}
set(value) {
this.invocationRetryPolicy(value)
}

/** Initial delay before the first retry attempt. If unset, server defaults apply. */
var InvocationRetryPolicy.Builder.initialInterval: Duration?
get() {
return this.initialInterval()?.toKotlinDuration()
}
set(value) {
this.initialInterval(value?.toJavaDuration())
}

/** Exponential backoff multiplier used to compute the next retry delay. */
var InvocationRetryPolicy.Builder.exponentiationFactor: Double?
get() {
return this.exponentiationFactor()
}
set(value) {
this.exponentiationFactor(value)
}

/** Upper bound for any computed retry delay. */
var InvocationRetryPolicy.Builder.maxInterval: Duration?
get() {
return this.maxInterval()?.toKotlinDuration()
}
set(value) {
this.maxInterval(value?.toJavaDuration())
}

/**
* Maximum number of attempts before giving up retrying.
*
* The initial call counts as the first attempt; retries increment the count by 1. When giving up,
* the behavior defined with [onMaxAttempts] will be applied.
*
* @see InvocationRetryPolicy.OnMaxAttempts
*/
var InvocationRetryPolicy.Builder.maxAttempts: Int?
get() {
return this.maxAttempts()
}
set(value) {
this.maxAttempts(value)
}

/**
* Behavior when reaching max attempts.
*
* @see InvocationRetryPolicy.OnMaxAttempts
*/
var InvocationRetryPolicy.Builder.onMaxAttempts: InvocationRetryPolicy.OnMaxAttempts?
get() {
return this.onMaxAttempts()
}
set(value) {
this.onMaxAttempts(value)
}

/** [InvocationRetryPolicy] builder function. */
fun invocationRetryPolicy(init: InvocationRetryPolicy.Builder.() -> Unit): InvocationRetryPolicy {
val builder = InvocationRetryPolicy.builder()
builder.init()
return builder.build()
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public final class HandlerDefinition<REQ, RES> {
private final @Nullable Duration journalRetention;
private final @Nullable Boolean ingressPrivate;
private final @Nullable Boolean enableLazyState;
private final @Nullable InvocationRetryPolicy invocationRetryPolicy;

HandlerDefinition(
String name,
Expand All @@ -51,7 +52,8 @@ public final class HandlerDefinition<REQ, RES> {
@Nullable Duration workflowRetention,
@Nullable Duration journalRetention,
@Nullable Boolean ingressPrivate,
@Nullable Boolean enableLazyState) {
@Nullable Boolean enableLazyState,
@Nullable InvocationRetryPolicy invocationRetryPolicy) {
this.name = name;
this.handlerType = handlerType;
this.acceptContentType = acceptContentType;
Expand All @@ -67,6 +69,7 @@ public final class HandlerDefinition<REQ, RES> {
this.journalRetention = journalRetention;
this.ingressPrivate = ingressPrivate;
this.enableLazyState = enableLazyState;
this.invocationRetryPolicy = invocationRetryPolicy;
}

/**
Expand Down Expand Up @@ -181,6 +184,14 @@ public HandlerRunner<REQ, RES> getRunner() {
return enableLazyState;
}

/**
* @return Retry policy for all requests to this handler
* @see Configurator#invocationRetryPolicy(InvocationRetryPolicy)
*/
public @Nullable InvocationRetryPolicy getInvocationRetryPolicy() {
return invocationRetryPolicy;
}

public HandlerDefinition<REQ, RES> withAcceptContentType(String acceptContentType) {
return new HandlerDefinition<>(
name,
Expand All @@ -197,7 +208,8 @@ public HandlerDefinition<REQ, RES> withAcceptContentType(String acceptContentTyp
workflowRetention,
journalRetention,
ingressPrivate,
enableLazyState);
enableLazyState,
invocationRetryPolicy);
}

public HandlerDefinition<REQ, RES> withDocumentation(@Nullable String documentation) {
Expand All @@ -216,7 +228,8 @@ public HandlerDefinition<REQ, RES> withDocumentation(@Nullable String documentat
journalRetention,
workflowRetention,
ingressPrivate,
enableLazyState);
enableLazyState,
invocationRetryPolicy);
}

public HandlerDefinition<REQ, RES> withMetadata(Map<String, String> metadata) {
Expand All @@ -235,7 +248,8 @@ public HandlerDefinition<REQ, RES> withMetadata(Map<String, String> metadata) {
workflowRetention,
journalRetention,
ingressPrivate,
enableLazyState);
enableLazyState,
invocationRetryPolicy);
}

/**
Expand All @@ -255,7 +269,8 @@ public HandlerDefinition<REQ, RES> configure(
workflowRetention,
journalRetention,
ingressPrivate,
enableLazyState);
enableLazyState,
invocationRetryPolicy);
configurator.accept(configuratorObj);

return new HandlerDefinition<>(
Expand All @@ -273,7 +288,8 @@ public HandlerDefinition<REQ, RES> configure(
configuratorObj.workflowRetention,
configuratorObj.journalRetention,
configuratorObj.ingressPrivate,
configuratorObj.enableLazyState);
configuratorObj.enableLazyState,
configuratorObj.invocationRetryPolicy);
}

/** Configurator for a {@link HandlerDefinition}. */
Expand All @@ -290,6 +306,7 @@ public static final class Configurator {
private @Nullable Duration journalRetention;
private @Nullable Boolean ingressPrivate;
private @Nullable Boolean enableLazyState;
private @Nullable InvocationRetryPolicy invocationRetryPolicy;

private Configurator(
HandlerType handlerType,
Expand All @@ -302,7 +319,8 @@ private Configurator(
@Nullable Duration workflowRetention,
@Nullable Duration journalRetention,
@Nullable Boolean ingressPrivate,
@Nullable Boolean enableLazyState) {
@Nullable Boolean enableLazyState,
@Nullable InvocationRetryPolicy invocationRetryPolicy) {
this.handlerType = handlerType;
this.acceptContentType = acceptContentType;
this.documentation = documentation;
Expand All @@ -314,6 +332,7 @@ private Configurator(
this.journalRetention = journalRetention;
this.ingressPrivate = ingressPrivate;
this.enableLazyState = enableLazyState;
this.invocationRetryPolicy = invocationRetryPolicy;
}

/**
Expand Down Expand Up @@ -555,6 +574,37 @@ public Configurator enableLazyState(@Nullable Boolean enableLazyState) {
this.enableLazyState = enableLazyState;
return this;
}

/**
* @return configured invocation retry policy
* @see #invocationRetryPolicy(InvocationRetryPolicy)
*/
public @Nullable InvocationRetryPolicy invocationRetryPolicy() {
return invocationRetryPolicy;
}

/**
* Retry policy used by Restate when invoking this handler.
*
* <p><b>NOTE:</b> You can set this field only if you register this service against
* restate-server >= 1.5, otherwise the service discovery will fail.
*
* @see InvocationRetryPolicy
* @return this
*/
public Configurator invocationRetryPolicy(
@Nullable InvocationRetryPolicy invocationRetryPolicy) {
this.invocationRetryPolicy = invocationRetryPolicy;
return this;
}

/**
* @see #invocationRetryPolicy(InvocationRetryPolicy)
*/
public Configurator invocationRetryPolicy(InvocationRetryPolicy.Builder invocationRetryPolicy) {
this.invocationRetryPolicy = invocationRetryPolicy.build();
return this;
}
}

public static <REQ, RES> HandlerDefinition<REQ, RES> of(
Expand All @@ -578,6 +628,7 @@ public static <REQ, RES> HandlerDefinition<REQ, RES> of(
null,
null,
null,
null,
null);
}

Expand All @@ -598,7 +649,8 @@ && getHandlerType() == that.getHandlerType()
&& Objects.equals(getWorkflowRetention(), that.getWorkflowRetention())
&& Objects.equals(getJournalRetention(), that.getJournalRetention())
&& Objects.equals(getIngressPrivate(), that.getIngressPrivate())
&& Objects.equals(getEnableLazyState(), that.getEnableLazyState());
&& Objects.equals(getEnableLazyState(), that.getEnableLazyState())
&& Objects.equals(getInvocationRetryPolicy(), that.getInvocationRetryPolicy());
}

@Override
Expand All @@ -618,6 +670,7 @@ public int hashCode() {
getWorkflowRetention(),
getJournalRetention(),
getIngressPrivate(),
getEnableLazyState());
getEnableLazyState(),
getInvocationRetryPolicy());
}
}
Loading