diff --git a/sdk-spring-boot-starter/src/test/java/dev/restate/sdk/springboot/java/RestateHttpEndpointBeanDefaultsTest.java b/sdk-spring-boot-starter/src/test/java/dev/restate/sdk/springboot/java/RestateHttpEndpointBeanDefaultsTest.java new file mode 100644 index 00000000..815e081b --- /dev/null +++ b/sdk-spring-boot-starter/src/test/java/dev/restate/sdk/springboot/java/RestateHttpEndpointBeanDefaultsTest.java @@ -0,0 +1,80 @@ +// 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.java; + +import static org.assertj.core.api.Assertions.*; + +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 = { + RestateEndpointConfiguration.class, + RestateHttpConfiguration.class, + Greeter.class, + GreeterNewApi.class, + ServicesConfiguration.class + }, + properties = { + "restate.sdk.http.port=0", + // Default applied to both services + "restate.journal-retention=PT48H", + // Per-service override for greeterNewApi + "restate.components.greeterNewApi.journal-retention=PT72H", + "greetingPrefix=Hello " + }) +public class RestateHttpEndpointBeanDefaultsTest { + + @Autowired private RestateHttpEndpointBean restateHttpEndpointBean; + + @Test + public void defaultConfigShouldApplyToAllServicesAndPerServiceOverrideWins() + throws IOException, InterruptedException { + assertThat(restateHttpEndpointBean.isRunning()).isTrue(); + + var client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2).build(); + var response = + client.send( + HttpRequest.newBuilder() + .GET() + .version(HttpClient.Version.HTTP_2) + .uri( + URI.create( + "http://localhost:" + restateHttpEndpointBean.actualPort() + "/discover")) + .header("Accept", "application/vnd.restate.endpointmanifest.v4+json") + .build(), + HttpResponse.BodyHandlers.ofString()); + assertThat(response.statusCode()).isEqualTo(200); + + var endpointManifest = + new ObjectMapper().readValue(response.body(), EndpointManifestSchema.class); + + assertThat(endpointManifest.getServices()) + .extracting( + dev.restate.sdk.core.generated.manifest.Service::getName, + dev.restate.sdk.core.generated.manifest.Service::getJournalRetention) + .containsExactlyInAnyOrder( + // greeter gets the global default + tuple("greeter", Duration.ofHours(48).toMillis()), + // greeterNewApi gets its per-service override + tuple("greeterNewApi", Duration.ofHours(72).toMillis())); + } +} diff --git a/sdk-spring-boot/src/main/java/dev/restate/sdk/springboot/RestateComponentsProperties.java b/sdk-spring-boot/src/main/java/dev/restate/sdk/springboot/RestateComponentsProperties.java index dc82612a..979efa58 100644 --- a/sdk-spring-boot/src/main/java/dev/restate/sdk/springboot/RestateComponentsProperties.java +++ b/sdk-spring-boot/src/main/java/dev/restate/sdk/springboot/RestateComponentsProperties.java @@ -8,6 +8,7 @@ // https://github.com/restatedev/sdk-java/blob/main/LICENSE package dev.restate.sdk.springboot; +import java.time.Duration; import java.util.HashMap; import java.util.Map; import org.jspecify.annotations.Nullable; @@ -16,12 +17,21 @@ /** * Properties for configuring Restate services. * + *
Top-level fields (e.g. {@code restate.inactivity-timeout}) act as defaults applied to all + * services. Per-service configuration in {@link #getComponents()} takes precedence over these + * defaults. + * *
Example configuration in {@code application.properties}: * *
{@code
- * # Configuration for a service named "MyService"
+ * # Default configuration applied to all services
+ * restate.executor=myGlobalExecutor
+ * restate.inactivity-timeout=10m
+ * restate.retry-policy.max-attempts=5
+ *
+ * # Per-service configuration (overrides defaults)
* restate.components.MyService.executor=myServiceExecutor
- * restate.components.MyService.inactivity-timeout=10m
+ * restate.components.MyService.inactivity-timeout=5m
* restate.components.MyService.abort-timeout=1m
* restate.components.MyService.idempotency-retention=1d
* restate.components.MyService.journal-retention=7d
@@ -47,14 +57,23 @@
public class RestateComponentsProperties {
@Nullable private String executor;
+ @Nullable private String documentation;
+ @Nullable private Map metadata;
+ @Nullable private Duration inactivityTimeout;
+ @Nullable private Duration abortTimeout;
+ @Nullable private Duration idempotencyRetention;
+ @Nullable private Duration workflowRetention;
+ @Nullable private Duration journalRetention;
+ @Nullable private Boolean ingressPrivate;
+ @Nullable private Boolean enableLazyState;
+ @Nullable private RetryPolicyProperties retryPolicy;
- // Map keyed by function bean name (e.g. restate.function.my-function.inactivity-timeout)
+ // Map keyed by service name (e.g. restate.components.MyService.inactivity-timeout)
private Map components = new HashMap<>();
/**
* 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()}.
+ * services. Can be overridden per-service in {@link #getComponents()}.
*
* NOTE: This option is only used for Java services, not Kotlin services.
*
@@ -68,8 +87,7 @@ public class RestateComponentsProperties {
/**
* 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()}.
+ * services. Can be overridden per-service in {@link #getComponents()}.
*
*
NOTE: This option is only used for Java services, not Kotlin services.
*
@@ -82,7 +100,231 @@ public void setExecutor(@Nullable String executor) {
}
/**
- * Per-component configuration, keyed by component/service name.
+ * Default documentation for all services, as shown in the UI, Admin REST API, and the generated
+ * OpenAPI documentation. Can be overridden per-service in {@link #getComponents()}.
+ */
+ public @Nullable String getDocumentation() {
+ return documentation;
+ }
+
+ /**
+ * Default documentation for all services, as shown in the UI, Admin REST API, and the generated
+ * OpenAPI documentation. Can be overridden per-service in {@link #getComponents()}.
+ */
+ public void setDocumentation(@Nullable String documentation) {
+ this.documentation = documentation;
+ }
+
+ /**
+ * Default metadata for all services, as propagated in the Admin REST API. Can be overridden
+ * per-service in {@link #getComponents()}.
+ */
+ public @Nullable Map getMetadata() {
+ return metadata;
+ }
+
+ /**
+ * Default metadata for all services, as propagated in the Admin REST API. Can be overridden
+ * per-service in {@link #getComponents()}.
+ */
+ public void setMetadata(@Nullable Map metadata) {
+ this.metadata = metadata;
+ }
+
+ /**
+ * Default inactivity timeout for all services. Can be overridden per-service in {@link
+ * #getComponents()}.
+ *
+ * NOTE: You can set this field only if you register services against restate-server >=
+ * 1.4, otherwise service discovery will fail.
+ *
+ * @see RestateComponentProperties#getInactivityTimeout()
+ */
+ public @Nullable Duration getInactivityTimeout() {
+ return inactivityTimeout;
+ }
+
+ /**
+ * Default inactivity timeout for all services. Can be overridden per-service in {@link
+ * #getComponents()}.
+ *
+ *
NOTE: You can set this field only if you register services against restate-server >=
+ * 1.4, otherwise service discovery will fail.
+ */
+ public void setInactivityTimeout(@Nullable Duration inactivityTimeout) {
+ this.inactivityTimeout = inactivityTimeout;
+ }
+
+ /**
+ * Default abort timeout for all services. Can be overridden per-service in {@link
+ * #getComponents()}.
+ *
+ *
NOTE: You can set this field only if you register services against restate-server >=
+ * 1.4, otherwise service discovery will fail.
+ *
+ * @see RestateComponentProperties#getAbortTimeout()
+ */
+ public @Nullable Duration getAbortTimeout() {
+ return abortTimeout;
+ }
+
+ /**
+ * Default abort timeout for all services. Can be overridden per-service in {@link
+ * #getComponents()}.
+ *
+ *
NOTE: You can set this field only if you register services against restate-server >=
+ * 1.4, otherwise service discovery will fail.
+ */
+ public void setAbortTimeout(@Nullable Duration abortTimeout) {
+ this.abortTimeout = abortTimeout;
+ }
+
+ /**
+ * Default idempotency retention for all services. Can be overridden per-service in {@link
+ * #getComponents()}.
+ *
+ *
NOTE: You can set this field only if you register services against restate-server >=
+ * 1.4, otherwise service discovery will fail.
+ *
+ * @see RestateComponentProperties#getIdempotencyRetention()
+ */
+ public @Nullable Duration getIdempotencyRetention() {
+ return idempotencyRetention;
+ }
+
+ /**
+ * Default idempotency retention for all services. Can be overridden per-service in {@link
+ * #getComponents()}.
+ *
+ *
NOTE: You can set this field only if you register services against restate-server >=
+ * 1.4, otherwise service discovery will fail.
+ */
+ public void setIdempotencyRetention(@Nullable Duration idempotencyRetention) {
+ this.idempotencyRetention = idempotencyRetention;
+ }
+
+ /**
+ * Default workflow retention for all workflow services. Can be overridden per-service in {@link
+ * #getComponents()}.
+ *
+ *
NOTE: You can set this field only if you register services against restate-server >=
+ * 1.4, otherwise service discovery will fail.
+ *
+ * @see RestateComponentProperties#getWorkflowRetention()
+ */
+ public @Nullable Duration getWorkflowRetention() {
+ return workflowRetention;
+ }
+
+ /**
+ * Default workflow retention for all workflow services. Can be overridden per-service in {@link
+ * #getComponents()}.
+ *
+ *
NOTE: You can set this field only if you register services against restate-server >=
+ * 1.4, otherwise service discovery will fail.
+ */
+ public void setWorkflowRetention(@Nullable Duration workflowRetention) {
+ this.workflowRetention = workflowRetention;
+ }
+
+ /**
+ * Default journal retention for all services. Can be overridden per-service in {@link
+ * #getComponents()}.
+ *
+ *
NOTE: You can set this field only if you register services against restate-server >=
+ * 1.4, otherwise service discovery will fail.
+ *
+ * @see RestateComponentProperties#getJournalRetention()
+ */
+ public @Nullable Duration getJournalRetention() {
+ return journalRetention;
+ }
+
+ /**
+ * Default journal retention for all services. Can be overridden per-service in {@link
+ * #getComponents()}.
+ *
+ *
NOTE: You can set this field only if you register services against restate-server >=
+ * 1.4, otherwise service discovery will fail.
+ */
+ public void setJournalRetention(@Nullable Duration journalRetention) {
+ this.journalRetention = journalRetention;
+ }
+
+ /**
+ * Default ingress-private setting for all services. Can be overridden per-service in {@link
+ * #getComponents()}.
+ *
+ *
NOTE: You can set this field only if you register services against restate-server >=
+ * 1.4, otherwise service discovery will fail.
+ *
+ * @see RestateComponentProperties#getIngressPrivate()
+ */
+ public @Nullable Boolean getIngressPrivate() {
+ return ingressPrivate;
+ }
+
+ /**
+ * Default ingress-private setting for all services. Can be overridden per-service in {@link
+ * #getComponents()}.
+ *
+ *
NOTE: You can set this field only if you register services against restate-server >=
+ * 1.4, otherwise service discovery will fail.
+ */
+ public void setIngressPrivate(@Nullable Boolean ingressPrivate) {
+ this.ingressPrivate = ingressPrivate;
+ }
+
+ /**
+ * Default lazy-state setting for all services. Can be overridden per-service in {@link
+ * #getComponents()}.
+ *
+ *
NOTE: You can set this field only if you register services against restate-server >=
+ * 1.4, otherwise service discovery will fail.
+ *
+ * @see RestateComponentProperties#getEnableLazyState()
+ */
+ public @Nullable Boolean getEnableLazyState() {
+ return enableLazyState;
+ }
+
+ /**
+ * Default lazy-state setting for all services. Can be overridden per-service in {@link
+ * #getComponents()}.
+ *
+ *
NOTE: You can set this field only if you register services against restate-server >=
+ * 1.4, otherwise service discovery will fail.
+ */
+ public void setEnableLazyState(@Nullable Boolean enableLazyState) {
+ this.enableLazyState = enableLazyState;
+ }
+
+ /**
+ * Default retry policy for all services. Can be overridden per-service in {@link
+ * #getComponents()}.
+ *
+ *
NOTE: You can set this field only if you register services against restate-server >=
+ * 1.5, otherwise service discovery will fail.
+ *
+ * @see RestateComponentProperties#getRetryPolicy()
+ */
+ public @Nullable RetryPolicyProperties getRetryPolicy() {
+ return retryPolicy;
+ }
+
+ /**
+ * Default retry policy for all services. Can be overridden per-service in {@link
+ * #getComponents()}.
+ *
+ *
NOTE: You can set this field only if you register services against restate-server >=
+ * 1.5, otherwise service discovery will fail.
+ */
+ public void setRetryPolicy(@Nullable RetryPolicyProperties retryPolicy) {
+ this.retryPolicy = retryPolicy;
+ }
+
+ /**
+ * Per-component configuration, keyed by component/service name. Overrides any top-level defaults.
*
*
Example configuration in {@code application.properties}:
*
@@ -96,7 +338,7 @@ public Map getComponents() {
}
/**
- * Per-component configuration, keyed by component/service name.
+ * Per-component configuration, keyed by component/service name. Overrides any top-level defaults.
*
* Example configuration in {@code application.properties}:
*
diff --git a/sdk-spring-boot/src/main/java/dev/restate/sdk/springboot/RestateEndpointConfiguration.java b/sdk-spring-boot/src/main/java/dev/restate/sdk/springboot/RestateEndpointConfiguration.java
index aabc94f4..57c6f000 100644
--- a/sdk-spring-boot/src/main/java/dev/restate/sdk/springboot/RestateEndpointConfiguration.java
+++ b/sdk-spring-boot/src/main/java/dev/restate/sdk/springboot/RestateEndpointConfiguration.java
@@ -94,7 +94,9 @@ public class RestateEndpointConfiguration {
var isKotlinClass = ReflectionUtils.isKotlinClass(restateServiceDefinitionClazz);
var handlerOptions = javaRunnerOptions;
- Consumer configurator = conf -> {};
+ // Apply global defaults first, per-service config layered on top
+ Consumer configurator =
+ conf -> configureServiceDefaults(conf, restateComponentsProperties);
var componentProperties = restateComponentsProperties.getComponents().get(serviceName);
if (componentProperties != null) {
@@ -108,7 +110,9 @@ public class RestateEndpointConfiguration {
handlerOptions =
createHandlerRunnerOptions(applicationContext, componentProperties.getExecutor());
}
- configurator = conf -> configureService(conf, componentProperties);
+ final var finalComponentProperties = componentProperties;
+ configurator =
+ combine(configurator, conf -> configureService(conf, finalComponentProperties));
}
// Check the configurator on the annotation as well
@@ -195,6 +199,40 @@ public static void configureService(
}
}
+ private static void configureServiceDefaults(
+ ServiceDefinition.Configurator configurator, RestateComponentsProperties properties) {
+ if (properties.getDocumentation() != null) {
+ configurator.documentation(properties.getDocumentation());
+ }
+ if (properties.getMetadata() != null) {
+ configurator.metadata(properties.getMetadata());
+ }
+ if (properties.getInactivityTimeout() != null) {
+ configurator.inactivityTimeout(properties.getInactivityTimeout());
+ }
+ if (properties.getAbortTimeout() != null) {
+ configurator.abortTimeout(properties.getAbortTimeout());
+ }
+ if (properties.getIdempotencyRetention() != null) {
+ configurator.idempotencyRetention(properties.getIdempotencyRetention());
+ }
+ if (properties.getWorkflowRetention() != null) {
+ configurator.workflowRetention(properties.getWorkflowRetention());
+ }
+ if (properties.getJournalRetention() != null) {
+ configurator.journalRetention(properties.getJournalRetention());
+ }
+ if (properties.getIngressPrivate() != null) {
+ configurator.ingressPrivate(properties.getIngressPrivate());
+ }
+ if (properties.getEnableLazyState() != null) {
+ configurator.enableLazyState(properties.getEnableLazyState());
+ }
+ if (properties.getRetryPolicy() != null) {
+ configurator.invocationRetryPolicy(convertRetryPolicy(properties.getRetryPolicy()));
+ }
+ }
+
private static void configureHandler(
HandlerDefinition.Configurator configurator, RestateHandlerProperties properties) {
if (properties.getDocumentation() != null) {