Skip to content

Commit 3393460

Browse files
Retry policy (#540)
* Add junie file * Add new retry policy options
1 parent 727a856 commit 3393460

File tree

9 files changed

+732
-76
lines changed

9 files changed

+732
-76
lines changed

.junie/guidelines.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
Project-specific development guidelines
2+
3+
Scope
4+
- 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.
5+
6+
Build and configuration
7+
- JDK/Toolchains
8+
- 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.
9+
- You do NOT need a locally installed JDK 17 if your Gradle has toolchains enabled; Gradle will download it.
10+
- Gradle wrapper
11+
- Always use the provided wrapper: ./gradlew ...
12+
- Wrapper version: see gradle/wrapper/gradle-wrapper.properties (8.x). CI uses the wrapper as well.
13+
- Root build highlights (build.gradle.kts)
14+
- Versioning via Gradle Version Catalog: gradle/libs.versions.toml. The SDK version is libs.versions.restate.
15+
- 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.
16+
- Dokka is applied to most subprojects for Kotlin docs; aggregated Javadocs live in :sdk-aggregated-javadocs.
17+
- 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.
18+
- Subprojects layout
19+
- 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.
20+
- Java/Kotlin conventions are centralized in buildSrc:
21+
- java-conventions.gradle.kts: toolchain 17, JUnit Platform, Spotless with googleJavaFormat.
22+
- kotlin-conventions.gradle.kts: toolchain 17, JUnit Platform, Spotless with ktfmt, license headers.
23+
24+
Testing
25+
- Frameworks and dependencies
26+
- JUnit 5 Platform is enabled globally (tasks.withType<Test> { useJUnitPlatform() }).
27+
- AssertJ is available broadly via the version catalog and commonly applied in modules.
28+
- 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.
29+
- Running tests
30+
- All modules: ./gradlew test
31+
- Single module: ./gradlew :common:test (replace :common with the desired module)
32+
- Single class or method: ./gradlew :common:test --tests 'dev.restate.common.SomethingTest' or --tests 'dev.restate.common.SomethingTest.methodName'
33+
- 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.
34+
- Restate integration testing (sdk-testing)
35+
- Annotate your JUnit 5 test class with @RestateTest to bootstrap a Restate runtime in a container and register your services.
36+
- Example sketch:
37+
@RestateTest(containerImage = "ghcr.io/restatedev/restate:main")
38+
class CounterTest { /* fields annotated with @BindService, inject @RestateClient, etc. */ }
39+
- 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.
40+
- Under the hood, RestateRunner uses Testcontainers and opens ports 8080 (ingress) and 9070 (admin). Docker must be available.
41+
- Adding tests
42+
- Java tests: place under src/test/java and name *Test.java (JUnit 5). Kotlin tests under src/test/kotlin.
43+
- 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.
44+
- For Restate-based tests, add a dependency on sdk-testing if not already present and use the annotations provided in dev.restate.sdk.testing.*.
45+
- Example test run (verified locally)
46+
- A simple JUnit 5 test was created temporarily in :common and executed via:
47+
./gradlew :common:test --no-daemon
48+
- The build succeeded. The temporary test file was then removed to avoid polluting the repo.
49+
50+
Development and debugging tips
51+
- Formatting and license headers
52+
- Run formatting checks: ./gradlew spotlessCheck
53+
- Apply formatting and headers: ./gradlew spotlessApply
54+
- Any newly added source files must include the license header from config/license-header (Spotless can apply it).
55+
- Dependency and license compliance
56+
- 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.
57+
- Version management
58+
- 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.
59+
- Docs
60+
- Aggregated Javadoc: ./gradlew :sdk-aggregated-javadocs:javadoc
61+
- Kotlin docs: ./gradlew dokkaHtmlMultiModule
62+
- Docker-based tests
63+
- 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
64+
- IDE setup
65+
- 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.
66+
67+
Troubleshooting
68+
- 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.
69+
- 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.
70+
71+
Housekeeping
72+
- Do not commit temporary tests used for local verification; keep the tree clean.
73+
- Before publishing or opening PRs, run: ./gradlew clean build

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

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

1111
import dev.restate.sdk.endpoint.Endpoint
1212
import dev.restate.sdk.endpoint.definition.HandlerDefinition
13+
import dev.restate.sdk.endpoint.definition.InvocationRetryPolicy
1314
import dev.restate.sdk.endpoint.definition.ServiceDefinition
1415
import kotlin.time.Duration
1516
import kotlin.time.toJavaDuration
@@ -150,6 +151,22 @@ var ServiceDefinition.Configurator.ingressPrivate: Boolean?
150151
this.ingressPrivate(value)
151152
}
152153

154+
/**
155+
* Retry policy used by Restate when invoking this service.
156+
*
157+
* <p><b>NOTE:</b> You can set this field only if you register this service against
158+
* restate-server >= 1.5, otherwise the service discovery will fail.
159+
*
160+
* @see InvocationRetryPolicy
161+
*/
162+
var ServiceDefinition.Configurator.invocationRetryPolicy: InvocationRetryPolicy?
163+
get() {
164+
return this.invocationRetryPolicy()
165+
}
166+
set(value) {
167+
this.invocationRetryPolicy(value)
168+
}
169+
153170
/**
154171
* Set the acceptable content type when ingesting HTTP requests. Wildcards can be used, e.g.
155172
* `application/*` or `*/*`.
@@ -298,3 +315,82 @@ var HandlerDefinition.Configurator.enableLazyState: Boolean?
298315
set(value) {
299316
this.enableLazyState(value)
300317
}
318+
319+
/**
320+
* Retry policy used by Restate when invoking this handler.
321+
*
322+
* <p><b>NOTE:</b> You can set this field only if you register this service against
323+
* restate-server >= 1.5, otherwise the service discovery will fail.
324+
*
325+
* @see InvocationRetryPolicy
326+
*/
327+
var HandlerDefinition.Configurator.invocationRetryPolicy: InvocationRetryPolicy?
328+
get() {
329+
return this.invocationRetryPolicy()
330+
}
331+
set(value) {
332+
this.invocationRetryPolicy(value)
333+
}
334+
335+
/** Initial delay before the first retry attempt. If unset, server defaults apply. */
336+
var InvocationRetryPolicy.Builder.initialInterval: Duration?
337+
get() {
338+
return this.initialInterval()?.toKotlinDuration()
339+
}
340+
set(value) {
341+
this.initialInterval(value?.toJavaDuration())
342+
}
343+
344+
/** Exponential backoff multiplier used to compute the next retry delay. */
345+
var InvocationRetryPolicy.Builder.exponentiationFactor: Double?
346+
get() {
347+
return this.exponentiationFactor()
348+
}
349+
set(value) {
350+
this.exponentiationFactor(value)
351+
}
352+
353+
/** Upper bound for any computed retry delay. */
354+
var InvocationRetryPolicy.Builder.maxInterval: Duration?
355+
get() {
356+
return this.maxInterval()?.toKotlinDuration()
357+
}
358+
set(value) {
359+
this.maxInterval(value?.toJavaDuration())
360+
}
361+
362+
/**
363+
* Maximum number of attempts before giving up retrying.
364+
*
365+
* The initial call counts as the first attempt; retries increment the count by 1. When giving up,
366+
* the behavior defined with [onMaxAttempts] will be applied.
367+
*
368+
* @see InvocationRetryPolicy.OnMaxAttempts
369+
*/
370+
var InvocationRetryPolicy.Builder.maxAttempts: Int?
371+
get() {
372+
return this.maxAttempts()
373+
}
374+
set(value) {
375+
this.maxAttempts(value)
376+
}
377+
378+
/**
379+
* Behavior when reaching max attempts.
380+
*
381+
* @see InvocationRetryPolicy.OnMaxAttempts
382+
*/
383+
var InvocationRetryPolicy.Builder.onMaxAttempts: InvocationRetryPolicy.OnMaxAttempts?
384+
get() {
385+
return this.onMaxAttempts()
386+
}
387+
set(value) {
388+
this.onMaxAttempts(value)
389+
}
390+
391+
/** [InvocationRetryPolicy] builder function. */
392+
fun invocationRetryPolicy(init: InvocationRetryPolicy.Builder.() -> Unit): InvocationRetryPolicy {
393+
val builder = InvocationRetryPolicy.builder()
394+
builder.init()
395+
return builder.build()
396+
}

sdk-common/src/main/java/dev/restate/sdk/endpoint/definition/HandlerDefinition.java

Lines changed: 62 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ public final class HandlerDefinition<REQ, RES> {
3535
private final @Nullable Duration journalRetention;
3636
private final @Nullable Boolean ingressPrivate;
3737
private final @Nullable Boolean enableLazyState;
38+
private final @Nullable InvocationRetryPolicy invocationRetryPolicy;
3839

3940
HandlerDefinition(
4041
String name,
@@ -51,7 +52,8 @@ public final class HandlerDefinition<REQ, RES> {
5152
@Nullable Duration workflowRetention,
5253
@Nullable Duration journalRetention,
5354
@Nullable Boolean ingressPrivate,
54-
@Nullable Boolean enableLazyState) {
55+
@Nullable Boolean enableLazyState,
56+
@Nullable InvocationRetryPolicy invocationRetryPolicy) {
5557
this.name = name;
5658
this.handlerType = handlerType;
5759
this.acceptContentType = acceptContentType;
@@ -67,6 +69,7 @@ public final class HandlerDefinition<REQ, RES> {
6769
this.journalRetention = journalRetention;
6870
this.ingressPrivate = ingressPrivate;
6971
this.enableLazyState = enableLazyState;
72+
this.invocationRetryPolicy = invocationRetryPolicy;
7073
}
7174

7275
/**
@@ -181,6 +184,14 @@ public HandlerRunner<REQ, RES> getRunner() {
181184
return enableLazyState;
182185
}
183186

187+
/**
188+
* @return Retry policy for all requests to this handler
189+
* @see Configurator#invocationRetryPolicy(InvocationRetryPolicy)
190+
*/
191+
public @Nullable InvocationRetryPolicy getInvocationRetryPolicy() {
192+
return invocationRetryPolicy;
193+
}
194+
184195
public HandlerDefinition<REQ, RES> withAcceptContentType(String acceptContentType) {
185196
return new HandlerDefinition<>(
186197
name,
@@ -197,7 +208,8 @@ public HandlerDefinition<REQ, RES> withAcceptContentType(String acceptContentTyp
197208
workflowRetention,
198209
journalRetention,
199210
ingressPrivate,
200-
enableLazyState);
211+
enableLazyState,
212+
invocationRetryPolicy);
201213
}
202214

203215
public HandlerDefinition<REQ, RES> withDocumentation(@Nullable String documentation) {
@@ -216,7 +228,8 @@ public HandlerDefinition<REQ, RES> withDocumentation(@Nullable String documentat
216228
journalRetention,
217229
workflowRetention,
218230
ingressPrivate,
219-
enableLazyState);
231+
enableLazyState,
232+
invocationRetryPolicy);
220233
}
221234

222235
public HandlerDefinition<REQ, RES> withMetadata(Map<String, String> metadata) {
@@ -235,7 +248,8 @@ public HandlerDefinition<REQ, RES> withMetadata(Map<String, String> metadata) {
235248
workflowRetention,
236249
journalRetention,
237250
ingressPrivate,
238-
enableLazyState);
251+
enableLazyState,
252+
invocationRetryPolicy);
239253
}
240254

241255
/**
@@ -255,7 +269,8 @@ public HandlerDefinition<REQ, RES> configure(
255269
workflowRetention,
256270
journalRetention,
257271
ingressPrivate,
258-
enableLazyState);
272+
enableLazyState,
273+
invocationRetryPolicy);
259274
configurator.accept(configuratorObj);
260275

261276
return new HandlerDefinition<>(
@@ -273,7 +288,8 @@ public HandlerDefinition<REQ, RES> configure(
273288
configuratorObj.workflowRetention,
274289
configuratorObj.journalRetention,
275290
configuratorObj.ingressPrivate,
276-
configuratorObj.enableLazyState);
291+
configuratorObj.enableLazyState,
292+
configuratorObj.invocationRetryPolicy);
277293
}
278294

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

294311
private Configurator(
295312
HandlerType handlerType,
@@ -302,7 +319,8 @@ private Configurator(
302319
@Nullable Duration workflowRetention,
303320
@Nullable Duration journalRetention,
304321
@Nullable Boolean ingressPrivate,
305-
@Nullable Boolean enableLazyState) {
322+
@Nullable Boolean enableLazyState,
323+
@Nullable InvocationRetryPolicy invocationRetryPolicy) {
306324
this.handlerType = handlerType;
307325
this.acceptContentType = acceptContentType;
308326
this.documentation = documentation;
@@ -314,6 +332,7 @@ private Configurator(
314332
this.journalRetention = journalRetention;
315333
this.ingressPrivate = ingressPrivate;
316334
this.enableLazyState = enableLazyState;
335+
this.invocationRetryPolicy = invocationRetryPolicy;
317336
}
318337

319338
/**
@@ -555,6 +574,37 @@ public Configurator enableLazyState(@Nullable Boolean enableLazyState) {
555574
this.enableLazyState = enableLazyState;
556575
return this;
557576
}
577+
578+
/**
579+
* @return configured invocation retry policy
580+
* @see #invocationRetryPolicy(InvocationRetryPolicy)
581+
*/
582+
public @Nullable InvocationRetryPolicy invocationRetryPolicy() {
583+
return invocationRetryPolicy;
584+
}
585+
586+
/**
587+
* Retry policy used by Restate when invoking this handler.
588+
*
589+
* <p><b>NOTE:</b> You can set this field only if you register this service against
590+
* restate-server >= 1.5, otherwise the service discovery will fail.
591+
*
592+
* @see InvocationRetryPolicy
593+
* @return this
594+
*/
595+
public Configurator invocationRetryPolicy(
596+
@Nullable InvocationRetryPolicy invocationRetryPolicy) {
597+
this.invocationRetryPolicy = invocationRetryPolicy;
598+
return this;
599+
}
600+
601+
/**
602+
* @see #invocationRetryPolicy(InvocationRetryPolicy)
603+
*/
604+
public Configurator invocationRetryPolicy(InvocationRetryPolicy.Builder invocationRetryPolicy) {
605+
this.invocationRetryPolicy = invocationRetryPolicy.build();
606+
return this;
607+
}
558608
}
559609

560610
public static <REQ, RES> HandlerDefinition<REQ, RES> of(
@@ -578,6 +628,7 @@ public static <REQ, RES> HandlerDefinition<REQ, RES> of(
578628
null,
579629
null,
580630
null,
631+
null,
581632
null);
582633
}
583634

@@ -598,7 +649,8 @@ && getHandlerType() == that.getHandlerType()
598649
&& Objects.equals(getWorkflowRetention(), that.getWorkflowRetention())
599650
&& Objects.equals(getJournalRetention(), that.getJournalRetention())
600651
&& Objects.equals(getIngressPrivate(), that.getIngressPrivate())
601-
&& Objects.equals(getEnableLazyState(), that.getEnableLazyState());
652+
&& Objects.equals(getEnableLazyState(), that.getEnableLazyState())
653+
&& Objects.equals(getInvocationRetryPolicy(), that.getInvocationRetryPolicy());
602654
}
603655

604656
@Override
@@ -618,6 +670,7 @@ public int hashCode() {
618670
getWorkflowRetention(),
619671
getJournalRetention(),
620672
getIngressPrivate(),
621-
getEnableLazyState());
673+
getEnableLazyState(),
674+
getInvocationRetryPolicy());
622675
}
623676
}

0 commit comments

Comments
 (0)