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) {