diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1bf856878..173911a9a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -177,6 +177,12 @@ [libraries.spring-boot-starter-test.version] ref = 'spring-boot' + [libraries.spring-boot-configuration-processor] + module = 'org.springframework.boot:spring-boot-configuration-processor' + + [libraries.spring-boot-configuration-processor.version] + ref = 'spring-boot' + [libraries.vertx-core] module = 'io.vertx:vertx-core' @@ -237,6 +243,6 @@ protobuf = '4.29.3' restate = '2.6.0-SNAPSHOT' schema-kenerator = '2.1.2' - spring-boot = '3.4.9' + spring-boot = '3.5.10' vertx = '4.5.22' victools-json-schema = '4.38.0' diff --git a/sdk-spring-boot-kotlin-starter/src/test/kotlin/dev/restate/sdk/springboot/kotlin/RestateHttpEndpointBeanTest.kt b/sdk-spring-boot-kotlin-starter/src/test/kotlin/dev/restate/sdk/springboot/kotlin/RestateHttpEndpointBeanTest.kt index 165dc3148..08ba54cbe 100644 --- a/sdk-spring-boot-kotlin-starter/src/test/kotlin/dev/restate/sdk/springboot/kotlin/RestateHttpEndpointBeanTest.kt +++ b/sdk-spring-boot-kotlin-starter/src/test/kotlin/dev/restate/sdk/springboot/kotlin/RestateHttpEndpointBeanTest.kt @@ -10,6 +10,8 @@ package dev.restate.sdk.springboot.kotlin import com.fasterxml.jackson.databind.ObjectMapper import dev.restate.sdk.core.generated.manifest.EndpointManifestSchema +import dev.restate.sdk.springboot.RestateEndpointConfiguration +import dev.restate.sdk.springboot.RestateHttpConfiguration import dev.restate.sdk.springboot.RestateHttpEndpointBean import java.io.IOException import java.net.URI @@ -22,7 +24,8 @@ import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest @SpringBootTest( - classes = [RestateHttpEndpointBean::class, Greeter::class], + classes = + [RestateEndpointConfiguration::class, RestateHttpConfiguration::class, Greeter::class], properties = ["restate.sdk.http.port=0"], ) class RestateHttpEndpointBeanTest { diff --git a/sdk-spring-boot-starter/src/test/java/dev/restate/sdk/springboot/java/RestateHttpEndpointBeanTest.java b/sdk-spring-boot-starter/src/test/java/dev/restate/sdk/springboot/java/RestateHttpEndpointBeanTest.java index 5bce0cc9b..dcec198a9 100644 --- a/sdk-spring-boot-starter/src/test/java/dev/restate/sdk/springboot/java/RestateHttpEndpointBeanTest.java +++ b/sdk-spring-boot-starter/src/test/java/dev/restate/sdk/springboot/java/RestateHttpEndpointBeanTest.java @@ -12,19 +12,31 @@ import com.fasterxml.jackson.databind.ObjectMapper; import dev.restate.sdk.core.generated.manifest.EndpointManifestSchema; +import dev.restate.sdk.springboot.RestateEndpointConfiguration; +import dev.restate.sdk.springboot.RestateHttpConfiguration; import dev.restate.sdk.springboot.RestateHttpEndpointBean; import java.io.IOException; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.time.Duration; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest( - classes = {RestateHttpEndpointBean.class, Greeter.class, Configuration.class}, - properties = {"restate.sdk.http.port=0"}) + classes = { + RestateEndpointConfiguration.class, + RestateHttpConfiguration.class, + Greeter.class, + ServicesConfiguration.class + }, + properties = { + "restate.sdk.http.port=0", + "restate.components.greeter.journal-retention=PT48H", + "greetingPrefix=Hello " + }) public class RestateHttpEndpointBeanTest { @Autowired private RestateHttpEndpointBean restateHttpEndpointBean; @@ -56,7 +68,8 @@ public void httpEndpointShouldBeRunning() throws IOException, InterruptedExcepti assertThat(endpointManifest.getServices()) .extracting( dev.restate.sdk.core.generated.manifest.Service::getName, - dev.restate.sdk.core.generated.manifest.Service::getDocumentation) - .containsOnly(tuple("greeter", "blabla")); + dev.restate.sdk.core.generated.manifest.Service::getDocumentation, + dev.restate.sdk.core.generated.manifest.Service::getJournalRetention) + .containsOnly(tuple("greeter", "blabla", Duration.ofDays(2).toMillis())); } } diff --git a/sdk-spring-boot-starter/src/test/java/dev/restate/sdk/springboot/java/Configuration.java b/sdk-spring-boot-starter/src/test/java/dev/restate/sdk/springboot/java/ServicesConfiguration.java similarity index 55% rename from sdk-spring-boot-starter/src/test/java/dev/restate/sdk/springboot/java/Configuration.java rename to sdk-spring-boot-starter/src/test/java/dev/restate/sdk/springboot/java/ServicesConfiguration.java index e03618d09..2b6de0d8a 100644 --- a/sdk-spring-boot-starter/src/test/java/dev/restate/sdk/springboot/java/Configuration.java +++ b/sdk-spring-boot-starter/src/test/java/dev/restate/sdk/springboot/java/ServicesConfiguration.java @@ -8,12 +8,17 @@ // https://github.com/restatedev/sdk-java/blob/main/LICENSE package dev.restate.sdk.springboot.java; -import dev.restate.sdk.springboot.RestateServiceConfigurator; +import dev.restate.sdk.springboot.RestateComponentProperties; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class ServicesConfiguration { -public class Configuration { @Bean - public RestateServiceConfigurator greeterConfiguration() { - return configurator -> configurator.documentation("blabla"); + public RestateComponentProperties greeterConfiguration() { + var properties = new RestateComponentProperties(); + properties.setDocumentation("blabla"); + return properties; } } diff --git a/sdk-spring-boot/build.gradle.kts b/sdk-spring-boot/build.gradle.kts index 30700cd1e..9b0aa6f91 100644 --- a/sdk-spring-boot/build.gradle.kts +++ b/sdk-spring-boot/build.gradle.kts @@ -10,6 +10,9 @@ description = "Restate SDK Spring Boot integration" dependencies { compileOnly(libs.jspecify) + // This generates the metadata needed for the configuration properties + annotationProcessor(libs.spring.boot.configuration.processor) + val excludeJackson = fun ProjectDependency.() { // Let spring bring jackson in diff --git a/sdk-spring-boot/src/main/java/dev/restate/sdk/springboot/EnableRestate.java b/sdk-spring-boot/src/main/java/dev/restate/sdk/springboot/EnableRestate.java index b9534f759..086430445 100644 --- a/sdk-spring-boot/src/main/java/dev/restate/sdk/springboot/EnableRestate.java +++ b/sdk-spring-boot/src/main/java/dev/restate/sdk/springboot/EnableRestate.java @@ -22,5 +22,9 @@ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented -@Import({RestateHttpEndpointBean.class, RestateClientAutoConfiguration.class}) +@Import({ + RestateEndpointConfiguration.class, + RestateHttpConfiguration.class, + RestateClientAutoConfiguration.class +}) public @interface EnableRestate {} diff --git a/sdk-spring-boot/src/main/java/dev/restate/sdk/springboot/RestateComponent.java b/sdk-spring-boot/src/main/java/dev/restate/sdk/springboot/RestateComponent.java index f8af3d2e0..c83670f06 100644 --- a/sdk-spring-boot/src/main/java/dev/restate/sdk/springboot/RestateComponent.java +++ b/sdk-spring-boot/src/main/java/dev/restate/sdk/springboot/RestateComponent.java @@ -25,8 +25,9 @@ @Component public @interface RestateComponent { /** - * Bean name to use to configure this component. The bean MUST be an instance of {@link - * RestateServiceConfigurator}. + * Bean name to use to configure this component. + * + *
The bean MUST be an instance of {@link RestateComponentProperties}. */ String configuration() default ""; } diff --git a/sdk-spring-boot/src/main/java/dev/restate/sdk/springboot/RestateComponentProperties.java b/sdk-spring-boot/src/main/java/dev/restate/sdk/springboot/RestateComponentProperties.java new file mode 100644 index 000000000..0dcd42a5b --- /dev/null +++ b/sdk-spring-boot/src/main/java/dev/restate/sdk/springboot/RestateComponentProperties.java @@ -0,0 +1,398 @@ +// 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.springboot; + +import java.time.Duration; +import java.util.Map; +import java.util.Objects; +import org.jspecify.annotations.Nullable; + +/** + * Configuration properties for a Restate service/component. + * + *
These properties can be used to configure Restate services via Spring configuration files.
+ */
+public class RestateComponentProperties {
+
+ @Nullable private String executor;
+ @Nullable private String documentation;
+ @Nullable private Map NOTE: This option is only used for Java services, not Kotlin services.
+ *
+ * If not specified (neither here nor globally), virtual threads are used for Java >= 21,
+ * otherwise {@link java.util.concurrent.Executors#newCachedThreadPool()} is used. See {@code
+ * HandlerRunner.Options.withExecutor()} for more details.
+ */
+ public @Nullable String getExecutor() {
+ return executor;
+ }
+
+ /**
+ * Name of the {@link java.util.concurrent.Executor} bean to use for running handlers of this
+ * service. If not specified, the global default from {@link RestateComponentsProperties} is used.
+ *
+ * NOTE: This option is only used for Java services, not Kotlin services.
+ *
+ * If not specified (neither here nor globally), virtual threads are used for Java >= 21,
+ * otherwise {@link java.util.concurrent.Executors#newCachedThreadPool()} is used. See {@code
+ * HandlerRunner.Options.withExecutor()} for more details.
+ */
+ public void setExecutor(@Nullable String executor) {
+ this.executor = executor;
+ }
+
+ /**
+ * Documentation as shown in the UI, Admin REST API, and the generated OpenAPI documentation of
+ * this service.
+ */
+ public @Nullable String getDocumentation() {
+ return documentation;
+ }
+
+ /**
+ * Documentation as shown in the UI, Admin REST API, and the generated OpenAPI documentation of
+ * this service.
+ */
+ public void setDocumentation(@Nullable String documentation) {
+ this.documentation = documentation;
+ }
+
+ /** Service metadata, as propagated in the Admin REST API. */
+ public @Nullable Map The {@link #getAbortTimeout()} is used to abort the invocation, in case it doesn't react to
+ * the request to suspend.
+ *
+ * This overrides the default inactivity timeout configured in the restate-server for all
+ * invocations to this service.
+ *
+ * NOTE: You can set this field only if you register this service against restate-server
+ * >= 1.4, otherwise the service discovery will fail.
+ */
+ public @Nullable Duration getInactivityTimeout() {
+ return inactivityTimeout;
+ }
+
+ /**
+ * This timer guards against stalled invocations. Once it expires, Restate triggers a graceful
+ * termination by asking the invocation to suspend (which preserves intermediate progress).
+ *
+ * The {@link #getAbortTimeout()} is used to abort the invocation, in case it doesn't react to
+ * the request to suspend.
+ *
+ * This overrides the default inactivity timeout configured in the restate-server for all
+ * invocations to this service.
+ *
+ * NOTE: You can set this field only if you register this service against restate-server
+ * >= 1.4, otherwise the service discovery will fail.
+ */
+ public void setInactivityTimeout(@Nullable Duration inactivityTimeout) {
+ this.inactivityTimeout = inactivityTimeout;
+ }
+
+ /**
+ * This timer guards against stalled service/handler invocations that are supposed to terminate.
+ * The abort timeout is started after the {@link #getInactivityTimeout()} has expired and the
+ * service/handler invocation has been asked to gracefully terminate. Once the timer expires, it
+ * will abort the service/handler invocation.
+ *
+ * This timer potentially interrupts user code. If the user code needs longer to
+ * gracefully terminate, then this value needs to be set accordingly.
+ *
+ * This overrides the default abort timeout configured in the restate-server for all
+ * invocations to this service.
+ *
+ * NOTE: You can set this field only if you register this service against restate-server
+ * >= 1.4, otherwise the service discovery will fail.
+ */
+ public @Nullable Duration getAbortTimeout() {
+ return abortTimeout;
+ }
+
+ /**
+ * This timer guards against stalled service/handler invocations that are supposed to terminate.
+ * The abort timeout is started after the {@link #getInactivityTimeout()} has expired and the
+ * service/handler invocation has been asked to gracefully terminate. Once the timer expires, it
+ * will abort the service/handler invocation.
+ *
+ * This timer potentially interrupts user code. If the user code needs longer to
+ * gracefully terminate, then this value needs to be set accordingly.
+ *
+ * This overrides the default abort timeout configured in the restate-server for all
+ * invocations to this service.
+ *
+ * NOTE: You can set this field only if you register this service against restate-server
+ * >= 1.4, otherwise the service discovery will fail.
+ */
+ public void setAbortTimeout(@Nullable Duration abortTimeout) {
+ this.abortTimeout = abortTimeout;
+ }
+
+ /**
+ * The retention duration of idempotent requests to this service.
+ *
+ * NOTE: You can set this field only if you register this service against restate-server
+ * >= 1.4, otherwise the service discovery will fail.
+ */
+ public @Nullable Duration getIdempotencyRetention() {
+ return idempotencyRetention;
+ }
+
+ /**
+ * The retention duration of idempotent requests to this service.
+ *
+ * NOTE: You can set this field only if you register this service against restate-server
+ * >= 1.4, otherwise the service discovery will fail.
+ */
+ public void setIdempotencyRetention(@Nullable Duration idempotencyRetention) {
+ this.idempotencyRetention = idempotencyRetention;
+ }
+
+ /**
+ * The retention duration of idempotent requests to this workflow service. This applies only to
+ * workflow services.
+ *
+ * NOTE: You can set this field only if you register this service against restate-server
+ * >= 1.4, otherwise the service discovery will fail.
+ */
+ public @Nullable Duration getWorkflowRetention() {
+ return workflowRetention;
+ }
+
+ /**
+ * The retention duration of idempotent requests to this workflow service. This applies only to
+ * workflow services.
+ *
+ * NOTE: You can set this field only if you register this service against restate-server
+ * >= 1.4, otherwise the service discovery will fail.
+ */
+ public void setWorkflowRetention(@Nullable Duration workflowRetention) {
+ this.workflowRetention = workflowRetention;
+ }
+
+ /**
+ * The journal retention. When set, this applies to all requests to all handlers of this service.
+ *
+ * In case the request has an idempotency key, the {@link #getIdempotencyRetention()} caps the
+ * journal retention time.
+ *
+ * NOTE: You can set this field only if you register this service against restate-server
+ * >= 1.4, otherwise the service discovery will fail.
+ */
+ public @Nullable Duration getJournalRetention() {
+ return journalRetention;
+ }
+
+ /**
+ * The journal retention. When set, this applies to all requests to all handlers of this service.
+ *
+ * In case the request has an idempotency key, the {@link #getIdempotencyRetention()} caps the
+ * journal retention time.
+ *
+ * NOTE: You can set this field only if you register this service against restate-server
+ * >= 1.4, otherwise the service discovery will fail.
+ */
+ public void setJournalRetention(@Nullable Duration journalRetention) {
+ this.journalRetention = journalRetention;
+ }
+
+ /**
+ * When set to {@code true} this service, with all its handlers, cannot be invoked from the
+ * restate-server HTTP and Kafka ingress, but only from other services.
+ *
+ * NOTE: You can set this field only if you register this service against restate-server
+ * >= 1.4, otherwise the service discovery will fail.
+ */
+ public @Nullable Boolean getIngressPrivate() {
+ return ingressPrivate;
+ }
+
+ /**
+ * When set to {@code true} this service, with all its handlers, cannot be invoked from the
+ * restate-server HTTP and Kafka ingress, but only from other services.
+ *
+ * NOTE: You can set this field only if you register this service against restate-server
+ * >= 1.4, otherwise the service discovery will fail.
+ */
+ public void setIngressPrivate(@Nullable Boolean ingressPrivate) {
+ this.ingressPrivate = ingressPrivate;
+ }
+
+ /**
+ * When set to {@code true}, lazy state will be enabled for all invocations to this service. This
+ * is relevant only for workflows and virtual objects.
+ *
+ * NOTE: You can set this field only if you register this service against restate-server
+ * >= 1.4, otherwise the service discovery will fail.
+ */
+ public @Nullable Boolean getEnableLazyState() {
+ return enableLazyState;
+ }
+
+ /**
+ * When set to {@code true}, lazy state will be enabled for all invocations to this service. This
+ * is relevant only for workflows and virtual objects.
+ *
+ * NOTE: You can set this field only if you register this service against restate-server
+ * >= 1.4, otherwise the service discovery will fail.
+ */
+ public void setEnableLazyState(@Nullable Boolean enableLazyState) {
+ this.enableLazyState = enableLazyState;
+ }
+
+ /**
+ * Retry policy used by Restate when invoking this service.
+ *
+ * NOTE: You can set this field only if you register this service against restate-server
+ * >= 1.5, otherwise the service discovery will fail.
+ */
+ public @Nullable RetryPolicyProperties getRetryPolicy() {
+ return retryPolicy;
+ }
+
+ /**
+ * Retry policy used by Restate when invoking this service.
+ *
+ * NOTE: You can set this field only if you register this service against restate-server
+ * >= 1.5, otherwise the service discovery will fail.
+ */
+ public void setRetryPolicy(@Nullable RetryPolicyProperties retryPolicy) {
+ this.retryPolicy = retryPolicy;
+ }
+
+ /** Per-handler configuration, keyed by handler name. */
+ public @Nullable Map Example configuration in {@code application.properties}:
+ *
+ * NOTE: This option is only used for Java services, not Kotlin services.
+ *
+ * If not specified, virtual threads are used for Java >= 21, otherwise {@link
+ * java.util.concurrent.Executors#newCachedThreadPool()} is used. See {@code
+ * HandlerRunner.Options.withExecutor()} for more details.
+ */
+ public @Nullable String getExecutor() {
+ return executor;
+ }
+
+ /**
+ * Name of the {@link java.util.concurrent.Executor} bean to use for running handlers of all
+ * services. This is the global default and can be overridden per-service in {@link
+ * #getComponents()}.
+ *
+ * NOTE: This option is only used for Java services, not Kotlin services.
+ *
+ * If not specified, virtual threads are used for Java >= 21, otherwise {@link
+ * java.util.concurrent.Executors#newCachedThreadPool()} is used. See {@code
+ * HandlerRunner.Options.withExecutor()} for more details.
+ */
+ public void setExecutor(@Nullable String executor) {
+ this.executor = executor;
+ }
+
+ /**
+ * Per-component configuration, keyed by component/service name.
+ *
+ * Example configuration in {@code application.properties}:
+ *
+ * Example configuration in {@code application.properties}:
+ *
+ * These properties can be used to configure individual handlers within a Restate service via
+ * Spring configuration files.
+ */
+public class RestateHandlerProperties {
+
+ @Nullable private String documentation;
+ @Nullable private Map The {@link #getAbortTimeout()} is used to abort the invocation, in case it doesn't react to
+ * the request to suspend.
+ *
+ * This overrides the inactivity timeout set for the service and the default set in
+ * restate-server.
+ *
+ * NOTE: You can set this field only if you register this service against restate-server
+ * >= 1.4, otherwise the service discovery will fail.
+ */
+ public @Nullable Duration getInactivityTimeout() {
+ return inactivityTimeout;
+ }
+
+ /**
+ * This timer guards against stalled invocations. Once it expires, Restate triggers a graceful
+ * termination by asking the invocation to suspend (which preserves intermediate progress).
+ *
+ * The {@link #getAbortTimeout()} is used to abort the invocation, in case it doesn't react to
+ * the request to suspend.
+ *
+ * This overrides the inactivity timeout set for the service and the default set in
+ * restate-server.
+ *
+ * NOTE: You can set this field only if you register this service against restate-server
+ * >= 1.4, otherwise the service discovery will fail.
+ */
+ public void setInactivityTimeout(@Nullable Duration inactivityTimeout) {
+ this.inactivityTimeout = inactivityTimeout;
+ }
+
+ /**
+ * This timer guards against stalled invocations that are supposed to terminate. The abort timeout
+ * is started after the {@link #getInactivityTimeout()} has expired and the invocation has been
+ * asked to gracefully terminate. Once the timer expires, it will abort the invocation.
+ *
+ * This timer potentially interrupts user code. If the user code needs longer to
+ * gracefully terminate, then this value needs to be set accordingly.
+ *
+ * This overrides the abort timeout set for the service and the default set in restate-server.
+ *
+ * NOTE: You can set this field only if you register this service against restate-server
+ * >= 1.4, otherwise the service discovery will fail.
+ */
+ public @Nullable Duration getAbortTimeout() {
+ return abortTimeout;
+ }
+
+ /**
+ * This timer guards against stalled invocations that are supposed to terminate. The abort timeout
+ * is started after the {@link #getInactivityTimeout()} has expired and the invocation has been
+ * asked to gracefully terminate. Once the timer expires, it will abort the invocation.
+ *
+ * This timer potentially interrupts user code. If the user code needs longer to
+ * gracefully terminate, then this value needs to be set accordingly.
+ *
+ * This overrides the abort timeout set for the service and the default set in restate-server.
+ *
+ * NOTE: You can set this field only if you register this service against restate-server
+ * >= 1.4, otherwise the service discovery will fail.
+ */
+ public void setAbortTimeout(@Nullable Duration abortTimeout) {
+ this.abortTimeout = abortTimeout;
+ }
+
+ /**
+ * The retention duration of idempotent requests to this handler.
+ *
+ * NOTE: You can set this field only if you register this service against restate-server
+ * >= 1.4, otherwise the service discovery will fail.
+ *
+ * NOTE: This cannot be set for workflow handlers. Use {@link #getWorkflowRetention()}
+ * instead.
+ */
+ public @Nullable Duration getIdempotencyRetention() {
+ return idempotencyRetention;
+ }
+
+ /**
+ * The retention duration of idempotent requests to this handler.
+ *
+ * NOTE: You can set this field only if you register this service against restate-server
+ * >= 1.4, otherwise the service discovery will fail.
+ *
+ * NOTE: This cannot be set for workflow handlers. Use {@link #setWorkflowRetention}
+ * instead.
+ */
+ public void setIdempotencyRetention(@Nullable Duration idempotencyRetention) {
+ this.idempotencyRetention = idempotencyRetention;
+ }
+
+ /**
+ * The retention duration for workflow handlers.
+ *
+ * NOTE: You can set this field only if you register this service against restate-server
+ * >= 1.4, otherwise the service discovery will fail.
+ *
+ * NOTE: This can only be set for workflow handlers.
+ */
+ public @Nullable Duration getWorkflowRetention() {
+ return workflowRetention;
+ }
+
+ /**
+ * The retention duration for workflow handlers.
+ *
+ * NOTE: You can set this field only if you register this service against restate-server
+ * >= 1.4, otherwise the service discovery will fail.
+ *
+ * NOTE: This can only be set for workflow handlers.
+ */
+ public void setWorkflowRetention(@Nullable Duration workflowRetention) {
+ this.workflowRetention = workflowRetention;
+ }
+
+ /**
+ * The journal retention for invocations to this handler.
+ *
+ * In case the request has an idempotency key, the {@link #getIdempotencyRetention()} caps the
+ * journal retention time.
+ *
+ * NOTE: You can set this field only if you register this service against restate-server
+ * >= 1.4, otherwise the service discovery will fail.
+ */
+ public @Nullable Duration getJournalRetention() {
+ return journalRetention;
+ }
+
+ /**
+ * The journal retention for invocations to this handler.
+ *
+ * In case the request has an idempotency key, the {@link #getIdempotencyRetention()} caps the
+ * journal retention time.
+ *
+ * NOTE: You can set this field only if you register this service against restate-server
+ * >= 1.4, otherwise the service discovery will fail.
+ */
+ public void setJournalRetention(@Nullable Duration journalRetention) {
+ this.journalRetention = journalRetention;
+ }
+
+ /**
+ * When set to {@code true} this handler cannot be invoked from the restate-server HTTP and Kafka
+ * ingress, but only from other services.
+ *
+ * NOTE: You can set this field only if you register this service against restate-server
+ * >= 1.4, otherwise the service discovery will fail.
+ */
+ public @Nullable Boolean getIngressPrivate() {
+ return ingressPrivate;
+ }
+
+ /**
+ * When set to {@code true} this handler cannot be invoked from the restate-server HTTP and Kafka
+ * ingress, but only from other services.
+ *
+ * NOTE: You can set this field only if you register this service against restate-server
+ * >= 1.4, otherwise the service discovery will fail.
+ */
+ public void setIngressPrivate(@Nullable Boolean ingressPrivate) {
+ this.ingressPrivate = ingressPrivate;
+ }
+
+ /**
+ * When set to {@code true}, lazy state will be enabled for all invocations to this handler. This
+ * is relevant only for workflows and virtual objects.
+ *
+ * NOTE: You can set this field only if you register this service against restate-server
+ * >= 1.4, otherwise the service discovery will fail.
+ */
+ public @Nullable Boolean getEnableLazyState() {
+ return enableLazyState;
+ }
+
+ /**
+ * When set to {@code true}, lazy state will be enabled for all invocations to this handler. This
+ * is relevant only for workflows and virtual objects.
+ *
+ * NOTE: You can set this field only if you register this service against restate-server
+ * >= 1.4, otherwise the service discovery will fail.
+ */
+ public void setEnableLazyState(@Nullable Boolean enableLazyState) {
+ this.enableLazyState = enableLazyState;
+ }
+
+ /**
+ * Retry policy used by Restate when invoking this handler.
+ *
+ * NOTE: You can set this field only if you register this service against restate-server
+ * >= 1.5, otherwise the service discovery will fail.
+ */
+ public @Nullable RetryPolicyProperties getRetryPolicy() {
+ return retryPolicy;
+ }
+
+ /**
+ * Retry policy used by Restate when invoking this handler.
+ *
+ * NOTE: You can set this field only if you register this service against restate-server
+ * >= 1.5, otherwise the service discovery will fail.
+ */
+ public void setRetryPolicy(@Nullable RetryPolicyProperties retryPolicy) {
+ this.retryPolicy = retryPolicy;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof RestateHandlerProperties that)) return false;
+ return Objects.equals(getDocumentation(), that.getDocumentation())
+ && Objects.equals(getMetadata(), that.getMetadata())
+ && Objects.equals(getInactivityTimeout(), that.getInactivityTimeout())
+ && Objects.equals(getAbortTimeout(), that.getAbortTimeout())
+ && Objects.equals(getIdempotencyRetention(), that.getIdempotencyRetention())
+ && Objects.equals(getWorkflowRetention(), that.getWorkflowRetention())
+ && Objects.equals(getJournalRetention(), that.getJournalRetention())
+ && Objects.equals(getIngressPrivate(), that.getIngressPrivate())
+ && Objects.equals(getEnableLazyState(), that.getEnableLazyState())
+ && Objects.equals(getRetryPolicy(), that.getRetryPolicy());
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(
+ getDocumentation(),
+ getMetadata(),
+ getInactivityTimeout(),
+ getAbortTimeout(),
+ getIdempotencyRetention(),
+ getWorkflowRetention(),
+ getJournalRetention(),
+ getIngressPrivate(),
+ getEnableLazyState(),
+ getRetryPolicy());
+ }
+
+ @Override
+ public String toString() {
+ return "RestateHandlerProperties{"
+ + "documentation='"
+ + documentation
+ + '\''
+ + ", metadata="
+ + metadata
+ + ", inactivityTimeout="
+ + inactivityTimeout
+ + ", abortTimeout="
+ + abortTimeout
+ + ", idempotencyRetention="
+ + idempotencyRetention
+ + ", workflowRetention="
+ + workflowRetention
+ + ", journalRetention="
+ + journalRetention
+ + ", ingressPrivate="
+ + ingressPrivate
+ + ", enableLazyState="
+ + enableLazyState
+ + ", retryPolicy="
+ + retryPolicy
+ + '}';
+ }
+}
diff --git a/sdk-spring-boot/src/main/java/dev/restate/sdk/springboot/RestateHttpConfiguration.java b/sdk-spring-boot/src/main/java/dev/restate/sdk/springboot/RestateHttpConfiguration.java
new file mode 100644
index 000000000..c7a104398
--- /dev/null
+++ b/sdk-spring-boot/src/main/java/dev/restate/sdk/springboot/RestateHttpConfiguration.java
@@ -0,0 +1,36 @@
+// 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.springboot;
+
+import dev.restate.sdk.endpoint.Endpoint;
+import org.jspecify.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+@EnableConfigurationProperties(RestateHttpServerProperties.class)
+public class RestateHttpConfiguration {
+
+ private final Logger logger = LoggerFactory.getLogger(getClass());
+
+ @Nullable
+ @Bean
+ RestateHttpEndpointBean restateHttpEndpointBean(
+ @Nullable Endpoint endpoint, RestateHttpServerProperties restateHttpServerProperties) {
+ if (endpoint == null) {
+ logger.info("No Endpoint was injected, SDK server will not start");
+ // Don't start anything if no service is registered
+ return null;
+ }
+ return new RestateHttpEndpointBean(endpoint, restateHttpServerProperties);
+ }
+}
diff --git a/sdk-spring-boot/src/main/java/dev/restate/sdk/springboot/RestateHttpEndpointBean.java b/sdk-spring-boot/src/main/java/dev/restate/sdk/springboot/RestateHttpEndpointBean.java
index 220328174..24e465d2a 100644
--- a/sdk-spring-boot/src/main/java/dev/restate/sdk/springboot/RestateHttpEndpointBean.java
+++ b/sdk-spring-boot/src/main/java/dev/restate/sdk/springboot/RestateHttpEndpointBean.java
@@ -17,46 +17,42 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
-import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.SmartLifecycle;
-import org.springframework.stereotype.Component;
-
-/**
- * Restate HTTP Endpoint serving {@link RestateComponent}.
- *
- * @see Component
- */
-@Component
-@EnableConfigurationProperties({RestateHttpServerProperties.class, RestateEndpointProperties.class})
+
+/** Restate HTTP Endpoint serving {@link Endpoint} */
public class RestateHttpEndpointBean implements InitializingBean, SmartLifecycle {
private final Logger logger = LoggerFactory.getLogger(getClass());
- private final ApplicationContext applicationContext;
- private final RestateEndpointProperties restateEndpointProperties;
private final RestateHttpServerProperties restateHttpServerProperties;
private volatile boolean running;
- private HttpServer server;
+ private final HttpServer server;
+
+ public RestateHttpEndpointBean(
+ Endpoint endpoint, RestateHttpServerProperties restateHttpServerProperties) {
+ this.restateHttpServerProperties = restateHttpServerProperties;
+ this.server =
+ RestateHttpServer.fromHandler(
+ HttpEndpointRequestHandler.fromEndpoint(
+ endpoint, this.restateHttpServerProperties.isDisableBidirectionalStreaming()));
+ }
+ @Deprecated
public RestateHttpEndpointBean(
ApplicationContext applicationContext,
RestateEndpointProperties restateEndpointProperties,
RestateHttpServerProperties restateHttpServerProperties) {
- this.applicationContext = applicationContext;
- this.restateEndpointProperties = restateEndpointProperties;
this.restateHttpServerProperties = restateHttpServerProperties;
- }
- @Override
- public void afterPropertiesSet() {
Map The bean MUST be an instance of {@link RestateComponentProperties}.
*/
@AliasFor(annotation = RestateComponent.class, attribute = "configuration")
String configuration() default "";
diff --git a/sdk-spring-boot/src/main/java/dev/restate/sdk/springboot/RestateServiceConfigurator.java b/sdk-spring-boot/src/main/java/dev/restate/sdk/springboot/RestateServiceConfigurator.java
index a46dee4cb..ba1c11b89 100644
--- a/sdk-spring-boot/src/main/java/dev/restate/sdk/springboot/RestateServiceConfigurator.java
+++ b/sdk-spring-boot/src/main/java/dev/restate/sdk/springboot/RestateServiceConfigurator.java
@@ -11,6 +11,13 @@
import dev.restate.sdk.endpoint.definition.ServiceDefinition;
import java.util.function.Consumer;
-/** Service configurator to be registered as */
+/**
+ * Service configurator to be registered as
+ *
+ * @deprecated Use {@link RestateComponentProperties} instead, or directly define the configuration
+ * in your {@code application.properties} file. See {@link RestateComponentsProperties} for more
+ * info.
+ */
@FunctionalInterface
+@Deprecated
public interface RestateServiceConfigurator extends Consumer The bean MUST be an instance of {@link RestateComponentProperties}.
*/
@AliasFor(annotation = RestateComponent.class, attribute = "configuration")
String configuration() default "";
diff --git a/sdk-spring-boot/src/main/java/dev/restate/sdk/springboot/RestateWorkflow.java b/sdk-spring-boot/src/main/java/dev/restate/sdk/springboot/RestateWorkflow.java
index 631959e55..85bf7ed93 100644
--- a/sdk-spring-boot/src/main/java/dev/restate/sdk/springboot/RestateWorkflow.java
+++ b/sdk-spring-boot/src/main/java/dev/restate/sdk/springboot/RestateWorkflow.java
@@ -28,8 +28,9 @@
public @interface RestateWorkflow {
/**
- * Bean name to use to configure this workflow. The bean MUST be an instance of {@link
- * RestateServiceConfigurator}.
+ * Bean name to use to configure this component.
+ *
+ * The bean MUST be an instance of {@link RestateComponentProperties}.
*/
@AliasFor(annotation = RestateComponent.class, attribute = "configuration")
String configuration() default "";
diff --git a/sdk-spring-boot/src/main/java/dev/restate/sdk/springboot/RetryPolicyProperties.java b/sdk-spring-boot/src/main/java/dev/restate/sdk/springboot/RetryPolicyProperties.java
new file mode 100644
index 000000000..3f9c79e07
--- /dev/null
+++ b/sdk-spring-boot/src/main/java/dev/restate/sdk/springboot/RetryPolicyProperties.java
@@ -0,0 +1,203 @@
+// 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.springboot;
+
+import java.time.Duration;
+import java.util.Objects;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * Configuration properties for Restate's retry policy when retrying failed handler invocations.
+ *
+ * This policy controls an exponential backoff with optional capping and a terminal action:
+ *
+ * Unset fields inherit the corresponding defaults from the Restate server configuration.
+ *
+ * @see dev.restate.sdk.endpoint.definition.InvocationRetryPolicy
+ */
+public class RetryPolicyProperties {
+
+ /** Behavior when retry policy reaches {@link #getMaxAttempts()} attempts. */
+ public enum OnMaxAttempts {
+ /**
+ * Pause the invocation once retries are exhausted. The invocation enters the paused state and
+ * can be manually resumed from the CLI or UI.
+ */
+ PAUSE,
+ /**
+ * Kill the invocation once retries are exhausted. The invocation will be marked as failed and
+ * will not be retried unless explicitly re-triggered by the caller.
+ */
+ KILL
+ }
+
+ @Nullable private Duration initialInterval;
+ @Nullable private Double exponentiationFactor;
+ @Nullable private Duration maxInterval;
+ @Nullable private Integer maxAttempts;
+ @Nullable private OnMaxAttempts onMaxAttempts;
+
+ public RetryPolicyProperties() {}
+
+ public RetryPolicyProperties(
+ @Nullable Duration initialInterval,
+ @Nullable Double exponentiationFactor,
+ @Nullable Duration maxInterval,
+ @Nullable Integer maxAttempts,
+ @Nullable OnMaxAttempts onMaxAttempts) {
+ this.initialInterval = initialInterval;
+ this.exponentiationFactor = exponentiationFactor;
+ this.maxInterval = maxInterval;
+ this.maxAttempts = maxAttempts;
+ this.onMaxAttempts = onMaxAttempts;
+ }
+
+ /**
+ * Initial delay before the first retry attempt.
+ *
+ * If unset, the server default is used.
+ */
+ public @Nullable Duration getInitialInterval() {
+ return initialInterval;
+ }
+
+ /**
+ * Initial delay before the first retry attempt.
+ *
+ * If unset, the server default is used.
+ */
+ public void setInitialInterval(@Nullable Duration initialInterval) {
+ this.initialInterval = initialInterval;
+ }
+
+ /**
+ * Exponential backoff multiplier used to compute the next retry delay.
+ *
+ * For attempt {@code n}, the next delay is roughly {@code previousDelay *
+ * exponentiationFactor}, capped by {@link #getMaxInterval()} if set.
+ */
+ public @Nullable Double getExponentiationFactor() {
+ return exponentiationFactor;
+ }
+
+ /**
+ * Exponential backoff multiplier used to compute the next retry delay.
+ *
+ * For attempt {@code n}, the next delay is roughly {@code previousDelay *
+ * exponentiationFactor}, capped by {@link #getMaxInterval()} if set.
+ */
+ public void setExponentiationFactor(@Nullable Double exponentiationFactor) {
+ this.exponentiationFactor = exponentiationFactor;
+ }
+
+ /**
+ * Upper bound for the computed retry delay.
+ *
+ * If set, any computed delay will not exceed this value.
+ */
+ public @Nullable Duration getMaxInterval() {
+ return maxInterval;
+ }
+
+ /**
+ * Upper bound for the computed retry delay.
+ *
+ * If set, any computed delay will not exceed this value.
+ */
+ public void setMaxInterval(@Nullable Duration maxInterval) {
+ this.maxInterval = maxInterval;
+ }
+
+ /**
+ * 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 {@link #getOnMaxAttempts()} will be applied.
+ *
+ * @see OnMaxAttempts
+ */
+ public @Nullable Integer getMaxAttempts() {
+ return maxAttempts;
+ }
+
+ /**
+ * 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 {@link #getOnMaxAttempts()} will be applied.
+ *
+ * @see OnMaxAttempts
+ */
+ public void setMaxAttempts(@Nullable Integer maxAttempts) {
+ this.maxAttempts = maxAttempts;
+ }
+
+ /**
+ * Behavior when the configured {@link #getMaxAttempts()} is reached.
+ *
+ * @see OnMaxAttempts
+ */
+ public @Nullable OnMaxAttempts getOnMaxAttempts() {
+ return onMaxAttempts;
+ }
+
+ /**
+ * Behavior when the configured {@link #getMaxAttempts()} is reached.
+ *
+ * @see OnMaxAttempts
+ */
+ public void setOnMaxAttempts(@Nullable OnMaxAttempts onMaxAttempts) {
+ this.onMaxAttempts = onMaxAttempts;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof RetryPolicyProperties that)) return false;
+ return Objects.equals(getInitialInterval(), that.getInitialInterval())
+ && Objects.equals(getExponentiationFactor(), that.getExponentiationFactor())
+ && Objects.equals(getMaxInterval(), that.getMaxInterval())
+ && Objects.equals(getMaxAttempts(), that.getMaxAttempts())
+ && getOnMaxAttempts() == that.getOnMaxAttempts();
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(
+ getInitialInterval(),
+ getExponentiationFactor(),
+ getMaxInterval(),
+ getMaxAttempts(),
+ getOnMaxAttempts());
+ }
+
+ @Override
+ public String toString() {
+ return "RetryPolicyProperties{"
+ + "initialInterval="
+ + initialInterval
+ + ", exponentiationFactor="
+ + exponentiationFactor
+ + ", maxInterval="
+ + maxInterval
+ + ", maxAttempts="
+ + maxAttempts
+ + ", onMaxAttempts="
+ + onMaxAttempts
+ + '}';
+ }
+}
{@code
+ * # Configuration for a service named "MyService"
+ * restate.components.MyService.executor=myServiceExecutor
+ * restate.components.MyService.inactivity-timeout=10m
+ * restate.components.MyService.abort-timeout=1m
+ * restate.components.MyService.idempotency-retention=1d
+ * restate.components.MyService.journal-retention=7d
+ * restate.components.MyService.ingress-private=false
+ * restate.components.MyService.enable-lazy-state=true
+ * restate.components.MyService.documentation=My service description
+ * restate.components.MyService.metadata.version=1.0
+ * restate.components.MyService.metadata.team=platform
+ * restate.components.MyService.retry-policy.initial-interval=100ms
+ * restate.components.MyService.retry-policy.exponentiation-factor=2.0
+ * restate.components.MyService.retry-policy.max-interval=10s
+ * restate.components.MyService.retry-policy.max-attempts=10
+ * restate.components.MyService.retry-policy.on-max-attempts=PAUSE
+ *
+ * # Per-handler configuration
+ * restate.components.MyService.handlers.myHandler.inactivity-timeout=5m
+ * restate.components.MyService.handlers.myHandler.ingress-private=true
+ * restate.components.MyService.handlers.myHandler.documentation=Handler description
+ * restate.components.MyService.handlers.myWorkflowHandler.workflow-retention=30d
+ * }
+ */
+@ConfigurationProperties(prefix = "restate")
+public class RestateComponentsProperties {
+
+ @Nullable private String executor;
+
+ // Map keyed by function bean name (e.g. restate.function.my-function.inactivity-timeout)
+ private Map
+ * restate.components.MyService.inactivity-timeout=10m
+ * restate.components.MyService.handlers.myHandler.ingress-private=true
+ *
+ */
+ public Map
+ * restate.components.MyService.inactivity-timeout=10m
+ * restate.components.MyService.handlers.myHandler.ingress-private=true
+ *
+ */
+ public void setComponents(Map
+ *
+ *
+ *