From 93b8ac4a623a007a70d93667f8fd7efdd3274e4a Mon Sep 17 00:00:00 2001 From: Cassie Coyle Date: Wed, 14 May 2025 16:58:40 -0500 Subject: [PATCH 01/12] Feat Add TLS & mTLS support for gRPC with root CA and insecure mode (#1361) * feat: Support for GRPC ssl Signed-off-by: Javier Aliaga * add tests Signed-off-by: Cassandra Coyle * fix CI Signed-off-by: Cassandra Coyle * add back else if Signed-off-by: Cassandra Coyle * channel cleanup Signed-off-by: Cassandra Coyle * add root ca support Signed-off-by: Cassandra Coyle * checkstyles Signed-off-by: Cassandra Coyle * add insecure Signed-off-by: Cassandra Coyle * fix checkstyles Signed-off-by: Cassandra Coyle * use InsecureTrustManagerFactory Signed-off-by: Cassandra Coyle * fix test Signed-off-by: Cassandra Coyle --------- Signed-off-by: Javier Aliaga Signed-off-by: Cassandra Coyle Co-authored-by: Javier Aliaga Signed-off-by: salaboy --- sdk/pom.xml | 17 + .../main/java/io/dapr/config/Properties.java | 35 ++ .../main/java/io/dapr/utils/NetworkUtils.java | 132 +++++- .../java/io/dapr/utils/NetworkUtilsTest.java | 447 +++++++++++++++++- 4 files changed, 611 insertions(+), 20 deletions(-) diff --git a/sdk/pom.xml b/sdk/pom.xml index e26fc7dae8..14cb9e08d5 100644 --- a/sdk/pom.xml +++ b/sdk/pom.xml @@ -132,6 +132,23 @@ assertj-core ${assertj.version} + + org.bouncycastle + bcprov-jdk15on + 1.70 + test + + + org.bouncycastle + bcpkix-jdk15on + 1.70 + test + + + io.grpc + grpc-netty-shaded + ${grpc.version} + diff --git a/sdk/src/main/java/io/dapr/config/Properties.java b/sdk/src/main/java/io/dapr/config/Properties.java index 14b0a4fb26..98cccf2c48 100644 --- a/sdk/src/main/java/io/dapr/config/Properties.java +++ b/sdk/src/main/java/io/dapr/config/Properties.java @@ -102,6 +102,41 @@ public class Properties { "DAPR_GRPC_PORT", DEFAULT_GRPC_PORT); + /** + * GRPC TLS cert path for Dapr after checking system property and environment variable. + */ + public static final Property GRPC_TLS_CERT_PATH = new StringProperty( + "dapr.grpc.tls.cert.path", + "DAPR_GRPC_TLS_CERT_PATH", + null); + + /** + * GRPC TLS key path for Dapr after checking system property and environment variable. + */ + public static final Property GRPC_TLS_KEY_PATH = new StringProperty( + "dapr.grpc.tls.key.path", + "DAPR_GRPC_TLS_KEY_PATH", + null); + + /** + * GRPC TLS CA cert path for Dapr after checking system property and environment variable. + * This is used for TLS connections to servers with self-signed certificates. + */ + public static final Property GRPC_TLS_CA_PATH = new StringProperty( + "dapr.grpc.tls.ca.path", + "DAPR_GRPC_TLS_CA_PATH", + null); + + /** + * Use insecure TLS mode which still uses TLS but doesn't verify certificates. + * This uses InsecureTrustManagerFactory to trust all certificates. + * This should only be used for testing or in secure environments. + */ + public static final Property GRPC_TLS_INSECURE = new BooleanProperty( + "dapr.grpc.tls.insecure", + "DAPR_GRPC_TLS_INSECURE", + false); + /** * GRPC endpoint for remote sidecar connectivity. */ diff --git a/sdk/src/main/java/io/dapr/utils/NetworkUtils.java b/sdk/src/main/java/io/dapr/utils/NetworkUtils.java index 6ce15782c8..522b3e5d7c 100644 --- a/sdk/src/main/java/io/dapr/utils/NetworkUtils.java +++ b/sdk/src/main/java/io/dapr/utils/NetworkUtils.java @@ -14,11 +14,21 @@ package io.dapr.utils; import io.dapr.config.Properties; +import io.dapr.exceptions.DaprError; +import io.dapr.exceptions.DaprException; +import io.grpc.ChannelCredentials; import io.grpc.ClientInterceptor; +import io.grpc.Grpc; import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; +import io.grpc.TlsChannelCredentials; +import io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts; +import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder; +import io.grpc.netty.shaded.io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import java.io.FileInputStream; import java.io.IOException; +import java.io.InputStream; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Socket; @@ -26,9 +36,12 @@ import static io.dapr.config.Properties.GRPC_ENDPOINT; import static io.dapr.config.Properties.GRPC_PORT; +import static io.dapr.config.Properties.GRPC_TLS_CA_PATH; +import static io.dapr.config.Properties.GRPC_TLS_CERT_PATH; +import static io.dapr.config.Properties.GRPC_TLS_INSECURE; +import static io.dapr.config.Properties.GRPC_TLS_KEY_PATH; import static io.dapr.config.Properties.SIDECAR_IP; - /** * Utility methods for network, internal to Dapr SDK. */ @@ -56,19 +69,20 @@ public final class NetworkUtils { private static final String GRPC_ENDPOINT_HOSTNAME_REGEX_PART = "(([A-Za-z0-9_\\-\\.]+)|(\\[" + IPV6_REGEX + "\\]))"; private static final String GRPC_ENDPOINT_DNS_AUTHORITY_REGEX_PART = - "(?dns://)(?" + GRPC_ENDPOINT_HOSTNAME_REGEX_PART + ":[0-9]+)?/"; + "(?dns://)(?" + + GRPC_ENDPOINT_HOSTNAME_REGEX_PART + ":[0-9]+)?/"; private static final String GRPC_ENDPOINT_PARAM_REGEX_PART = "(\\?(?tls\\=((true)|(false))))?"; - private static final String GRPC_ENDPOINT_SOCKET_REGEX_PART = - "(?((unix:)|(unix://)|(unix-abstract:))" + GRPC_ENDPOINT_FILENAME_REGEX_PART + ")"; + private static final String GRPC_ENDPOINT_SOCKET_REGEX_PART = "(?((unix:)|(unix://)|(unix-abstract:))" + + GRPC_ENDPOINT_FILENAME_REGEX_PART + ")"; - private static final String GRPC_ENDPOINT_VSOCKET_REGEX_PART = - "(?vsock:" + GRPC_ENDPOINT_HOSTNAME_REGEX_PART + ":[0-9]+)"; - private static final String GRPC_ENDPOINT_HOST_REGEX_PART = - "((?http://)|(?https://)|(?dns:)|(" + GRPC_ENDPOINT_DNS_AUTHORITY_REGEX_PART + "))?" - + "(?" + GRPC_ENDPOINT_HOSTNAME_REGEX_PART + ")?+" - + "(:(?[0-9]+))?"; + private static final String GRPC_ENDPOINT_VSOCKET_REGEX_PART = "(?vsock:" + GRPC_ENDPOINT_HOSTNAME_REGEX_PART + + ":[0-9]+)"; + private static final String GRPC_ENDPOINT_HOST_REGEX_PART = "((?http://)|(?https://)|(?dns:)|(" + + GRPC_ENDPOINT_DNS_AUTHORITY_REGEX_PART + "))?" + + "(?" + GRPC_ENDPOINT_HOSTNAME_REGEX_PART + ")?+" + + "(:(?[0-9]+))?"; private static final String GRPC_ENDPOINT_REGEX = "^(" + "(" + GRPC_ENDPOINT_HOST_REGEX_PART + ")|" @@ -107,17 +121,76 @@ public static void waitForSocket(String host, int port, int timeoutInMillisecond /** * Creates a GRPC managed channel. - * @param properties instance to set up the GrpcEndpoint + * + * @param properties instance to set up the GrpcEndpoint * @param interceptors Optional interceptors to add to the channel. * @return GRPC managed channel to communicate with the sidecar. */ public static ManagedChannel buildGrpcManagedChannel(Properties properties, ClientInterceptor... interceptors) { var settings = GrpcEndpointSettings.parse(properties); - ManagedChannelBuilder builder = ManagedChannelBuilder.forTarget(settings.endpoint) - .userAgent(Version.getSdkVersion()); - if (!settings.secure) { + + boolean insecureTls = properties.getValue(GRPC_TLS_INSECURE); + if (insecureTls) { + try { + ManagedChannelBuilder builder = NettyChannelBuilder.forTarget(settings.endpoint) + .sslContext(GrpcSslContexts.forClient() + .trustManager(InsecureTrustManagerFactory.INSTANCE) + .build()); + builder.userAgent(Version.getSdkVersion()); + if (interceptors != null && interceptors.length > 0) { + builder = builder.intercept(interceptors); + } + return builder.build(); + } catch (Exception e) { + throw new DaprException( + new DaprError().setErrorCode("TLS_CREDENTIALS_ERROR") + .setMessage("Failed to create insecure TLS credentials"), e); + } + } + + String clientKeyPath = settings.tlsPrivateKeyPath; + String clientCertPath = settings.tlsCertPath; + String caCertPath = settings.tlsCaPath; + + ManagedChannelBuilder builder = ManagedChannelBuilder.forTarget(settings.endpoint); + + if (clientCertPath != null && clientKeyPath != null) { + // mTLS case - using client cert and key, with optional CA cert for server authentication + try ( + InputStream clientCertInputStream = new FileInputStream(clientCertPath); + InputStream clientKeyInputStream = new FileInputStream(clientKeyPath); + InputStream caCertInputStream = caCertPath != null ? new FileInputStream(caCertPath) : null + ) { + TlsChannelCredentials.Builder builderCreds = TlsChannelCredentials.newBuilder() + .keyManager(clientCertInputStream, clientKeyInputStream); // For client authentication + if (caCertInputStream != null) { + builderCreds.trustManager(caCertInputStream); // For server authentication + } + ChannelCredentials credentials = builderCreds.build(); + builder = Grpc.newChannelBuilder(settings.endpoint, credentials); + } catch (IOException e) { + throw new DaprException( + new DaprError().setErrorCode("TLS_CREDENTIALS_ERROR") + .setMessage("Failed to create mTLS credentials" + (caCertPath != null ? " with CA cert" : "")), e); + } + } else if (caCertPath != null) { + // Simple TLS case - using CA cert only for server authentication + try (InputStream caCertInputStream = new FileInputStream(caCertPath)) { + ChannelCredentials credentials = TlsChannelCredentials.newBuilder() + .trustManager(caCertInputStream) + .build(); + builder = Grpc.newChannelBuilder(settings.endpoint, credentials); + } catch (IOException e) { + throw new DaprException( + new DaprError().setErrorCode("TLS_CREDENTIALS_ERROR") + .setMessage("Failed to create TLS credentials with CA cert"), e); + } + } else if (!settings.secure) { builder = builder.usePlaintext(); } + + builder.userAgent(Version.getSdkVersion()); + if (interceptors != null && interceptors.length > 0) { builder = builder.intercept(interceptors); } @@ -128,15 +201,26 @@ public static ManagedChannel buildGrpcManagedChannel(Properties properties, Clie static final class GrpcEndpointSettings { final String endpoint; final boolean secure; + final String tlsPrivateKeyPath; + final String tlsCertPath; + final String tlsCaPath; - private GrpcEndpointSettings(String endpoint, boolean secure) { + private GrpcEndpointSettings( + String endpoint, boolean secure, String tlsPrivateKeyPath, String tlsCertPath, String tlsCaPath) { this.endpoint = endpoint; this.secure = secure; + this.tlsPrivateKeyPath = tlsPrivateKeyPath; + this.tlsCertPath = tlsCertPath; + this.tlsCaPath = tlsCaPath; } static GrpcEndpointSettings parse(Properties properties) { String address = properties.getValue(SIDECAR_IP); int port = properties.getValue(GRPC_PORT); + String clientKeyPath = properties.getValue(GRPC_TLS_KEY_PATH); + String clientCertPath = properties.getValue(GRPC_TLS_CERT_PATH); + String caCertPath = properties.getValue(GRPC_TLS_CA_PATH); + boolean secure = false; String grpcEndpoint = properties.getValue(GRPC_ENDPOINT); if ((grpcEndpoint != null) && !grpcEndpoint.isEmpty()) { @@ -172,21 +256,31 @@ static GrpcEndpointSettings parse(Properties properties) { var authorityEndpoint = matcher.group("authorityEndpoint"); if (authorityEndpoint != null) { - return new GrpcEndpointSettings(String.format("dns://%s/%s:%d", authorityEndpoint, address, port), secure); + return new GrpcEndpointSettings( + String.format( + "dns://%s/%s:%d", + authorityEndpoint, + address, + port + ), secure, clientKeyPath, clientCertPath, caCertPath); } var socket = matcher.group("socket"); if (socket != null) { - return new GrpcEndpointSettings(socket, secure); + return new GrpcEndpointSettings(socket, secure, clientKeyPath, clientCertPath, caCertPath); } var vsocket = matcher.group("vsocket"); if (vsocket != null) { - return new GrpcEndpointSettings(vsocket, secure); + return new GrpcEndpointSettings(vsocket, secure, clientKeyPath, clientCertPath, caCertPath); } } - return new GrpcEndpointSettings(String.format("dns:///%s:%d", address, port), secure); + return new GrpcEndpointSettings(String.format( + "dns:///%s:%d", + address, + port + ), secure, clientKeyPath, clientCertPath, caCertPath); } } diff --git a/sdk/src/test/java/io/dapr/utils/NetworkUtilsTest.java b/sdk/src/test/java/io/dapr/utils/NetworkUtilsTest.java index f044cd728f..2b4929abd0 100644 --- a/sdk/src/test/java/io/dapr/utils/NetworkUtilsTest.java +++ b/sdk/src/test/java/io/dapr/utils/NetworkUtilsTest.java @@ -1,19 +1,92 @@ +/* + * Copyright 2025 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the sppecific language governing permissions and +limitations under the License. +*/ + package io.dapr.utils; import io.dapr.config.Properties; +import io.dapr.exceptions.DaprException; import io.grpc.ManagedChannel; import org.junit.Assert; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledOnOs; +import org.junit.jupiter.api.condition.OS; +import java.io.File; +import java.nio.file.Files; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.cert.X509Certificate; +import java.util.Date; +import java.util.List; import java.util.Map; - +import java.util.ArrayList; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; public class NetworkUtilsTest { private final int defaultGrpcPort = 50001; private final String defaultSidecarIP = "127.0.0.1"; private ManagedChannel channel; + private static final List channels = new ArrayList<>(); + + // Helper method to generate a self-signed certificate for testing + private static KeyPair generateKeyPair() throws Exception { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + keyPairGenerator.initialize(2048); + return keyPairGenerator.generateKeyPair(); + } + + private static X509Certificate generateCertificate(KeyPair keyPair) throws Exception { + X500Name issuer = new X500Name("CN=Test Certificate"); + X500Name subject = new X500Name("CN=Test Certificate"); + Date notBefore = new Date(System.currentTimeMillis() - 24 * 60 * 60 * 1000); + Date notAfter = new Date(System.currentTimeMillis() + 365 * 24 * 60 * 60 * 1000L); + SubjectPublicKeyInfo publicKeyInfo = SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded()); + X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder( + issuer, + java.math.BigInteger.valueOf(System.currentTimeMillis()), + notBefore, + notAfter, + subject, + publicKeyInfo + ); + + ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA").build(keyPair.getPrivate()); + X509Certificate cert = new JcaX509CertificateConverter().getCertificate(certBuilder.build(signer)); + return cert; + } + + private static void writeCertificateToFile(X509Certificate cert, File file) throws Exception { + String certPem = "-----BEGIN CERTIFICATE-----\n" + + java.util.Base64.getEncoder().encodeToString(cert.getEncoded()) + + "\n-----END CERTIFICATE-----"; + Files.write(file.toPath(), certPem.getBytes()); + } + + private static void writePrivateKeyToFile(KeyPair keyPair, File file) throws Exception { + String keyPem = "-----BEGIN PRIVATE KEY-----\n" + + java.util.Base64.getEncoder().encodeToString(keyPair.getPrivate().getEncoded()) + + "\n-----END PRIVATE KEY-----"; + Files.write(file.toPath(), keyPem.getBytes()); + } @AfterEach public void tearDown() { @@ -22,9 +95,20 @@ public void tearDown() { } } + @AfterAll + public static void tearDownAll() { + for (ManagedChannel ch : channels) { + if (ch != null && !ch.isShutdown()) { + ch.shutdown(); + } + } + channels.clear(); + } + @Test public void testBuildGrpcManagedChannel() { channel = NetworkUtils.buildGrpcManagedChannel(new Properties()); + channels.add(channel); String expectedAuthority = String.format("%s:%s", defaultSidecarIP, defaultGrpcPort); Assertions.assertEquals(expectedAuthority, channel.authority()); @@ -34,6 +118,7 @@ public void testBuildGrpcManagedChannel() { public void testBuildGrpcManagedChannel_httpEndpointNoPort() { var properties = new Properties(Map.of(Properties.GRPC_ENDPOINT.getName(), "http://example.com")); channel = NetworkUtils.buildGrpcManagedChannel(properties); + channels.add(channel); String expectedAuthority = "example.com:80"; Assertions.assertEquals(expectedAuthority, channel.authority()); @@ -43,6 +128,7 @@ public void testBuildGrpcManagedChannel_httpEndpointNoPort() { public void testBuildGrpcManagedChannel_httpEndpointWithPort() { var properties = new Properties(Map.of(Properties.GRPC_ENDPOINT.getName(), "http://example.com:3000")); channel = NetworkUtils.buildGrpcManagedChannel(properties); + channels.add(channel); String expectedAuthority = "example.com:3000"; Assertions.assertEquals(expectedAuthority, channel.authority()); @@ -52,6 +138,7 @@ public void testBuildGrpcManagedChannel_httpEndpointWithPort() { public void testBuildGrpcManagedChannel_httpsEndpointNoPort() { var properties = new Properties(Map.of(Properties.GRPC_ENDPOINT.getName(), "https://example.com")); channel = NetworkUtils.buildGrpcManagedChannel(properties); + channels.add(channel); String expectedAuthority = "example.com:443"; Assertions.assertEquals(expectedAuthority, channel.authority()); @@ -61,11 +148,236 @@ public void testBuildGrpcManagedChannel_httpsEndpointNoPort() { public void testBuildGrpcManagedChannel_httpsEndpointWithPort() { var properties = new Properties(Map.of(Properties.GRPC_ENDPOINT.getName(), "https://example.com:3000")); channel = NetworkUtils.buildGrpcManagedChannel(properties); + channels.add(channel); String expectedAuthority = "example.com:3000"; Assertions.assertEquals(expectedAuthority, channel.authority()); } + @Test + public void testBuildGrpcManagedChannelWithTls() throws Exception { + // Generate test certificate and key + KeyPair keyPair = generateKeyPair(); + X509Certificate cert = generateCertificate(keyPair); + + File certFile = File.createTempFile("test-cert", ".pem"); + File keyFile = File.createTempFile("test-key", ".pem"); + try { + writeCertificateToFile(cert, certFile); + writePrivateKeyToFile(keyPair, keyFile); + + var properties = new Properties(Map.of( + Properties.GRPC_TLS_CERT_PATH.getName(), certFile.getAbsolutePath(), + Properties.GRPC_TLS_KEY_PATH.getName(), keyFile.getAbsolutePath() + )); + + channel = NetworkUtils.buildGrpcManagedChannel(properties); + channels.add(channel); + String expectedAuthority = String.format("%s:%s", defaultSidecarIP, defaultGrpcPort); + Assertions.assertEquals(expectedAuthority, channel.authority()); + } finally { + certFile.delete(); + keyFile.delete(); + } + } + + @Test + public void testBuildGrpcManagedChannelWithTlsAndEndpoint() throws Exception { + // Generate test certificate and key + KeyPair keyPair = generateKeyPair(); + X509Certificate cert = generateCertificate(keyPair); + + File certFile = File.createTempFile("test-cert", ".pem"); + File keyFile = File.createTempFile("test-key", ".pem"); + try { + writeCertificateToFile(cert, certFile); + writePrivateKeyToFile(keyPair, keyFile); + + var properties = new Properties(Map.of( + Properties.GRPC_TLS_CERT_PATH.getName(), certFile.getAbsolutePath(), + Properties.GRPC_TLS_KEY_PATH.getName(), keyFile.getAbsolutePath(), + Properties.GRPC_ENDPOINT.getName(), "https://example.com:443" + )); + + channel = NetworkUtils.buildGrpcManagedChannel(properties); + channels.add(channel); + Assertions.assertEquals("example.com:443", channel.authority()); + } finally { + certFile.delete(); + keyFile.delete(); + } + } + + @Test + public void testBuildGrpcManagedChannelWithInvalidTlsCert() { + var properties = new Properties(Map.of( + Properties.GRPC_TLS_CERT_PATH.getName(), "/nonexistent/cert.pem", + Properties.GRPC_TLS_KEY_PATH.getName(), "/nonexistent/key.pem" + )); + + Assertions.assertThrows(DaprException.class, () -> { + NetworkUtils.buildGrpcManagedChannel(properties); + }); + } + + @Test + @EnabledOnOs({OS.LINUX, OS.MAC}) // Unix domain sockets are only supported on Linux and macOS + public void testBuildGrpcManagedChannelWithTlsAndUnixSocket() throws Exception { + // Skip test if Unix domain sockets are not supported + Assumptions.assumeTrue(System.getProperty("os.name").toLowerCase().contains("linux") || + System.getProperty("os.name").toLowerCase().contains("mac")); + + // Generate test certificate and key + KeyPair keyPair = generateKeyPair(); + X509Certificate cert = generateCertificate(keyPair); + + File certFile = File.createTempFile("test-cert", ".pem"); + File keyFile = File.createTempFile("test-key", ".pem"); + try { + writeCertificateToFile(cert, certFile); + writePrivateKeyToFile(keyPair, keyFile); + + var properties = new Properties(Map.of( + Properties.GRPC_TLS_CERT_PATH.getName(), certFile.getAbsolutePath(), + Properties.GRPC_TLS_KEY_PATH.getName(), keyFile.getAbsolutePath(), + Properties.GRPC_ENDPOINT.getName(), "unix:/tmp/test.sock" + )); + + // For Unix sockets, we expect an exception if the platform doesn't support it + try { + channel = NetworkUtils.buildGrpcManagedChannel(properties); + channels.add(channel); + // If we get here, Unix sockets are supported + Assertions.assertNotNull(channel.authority(), "Channel authority should not be null"); + } catch (Exception e) { + // If we get here, Unix sockets are not supported + Assertions.assertTrue(e.getMessage().contains("DomainSocketAddress")); + } + } finally { + certFile.delete(); + keyFile.delete(); + } + } + + @Test + public void testBuildGrpcManagedChannelWithTlsAndDnsAuthority() throws Exception { + // Generate test certificate and key + KeyPair keyPair = generateKeyPair(); + X509Certificate cert = generateCertificate(keyPair); + + File certFile = File.createTempFile("test-cert", ".pem"); + File keyFile = File.createTempFile("test-key", ".pem"); + try { + writeCertificateToFile(cert, certFile); + writePrivateKeyToFile(keyPair, keyFile); + + var properties = new Properties(Map.of( + Properties.GRPC_TLS_CERT_PATH.getName(), certFile.getAbsolutePath(), + Properties.GRPC_TLS_KEY_PATH.getName(), keyFile.getAbsolutePath(), + Properties.GRPC_ENDPOINT.getName(), "dns://authority:53/example.com:443" + )); + + channel = NetworkUtils.buildGrpcManagedChannel(properties); + channels.add(channel); + Assertions.assertEquals("example.com:443", channel.authority()); + } finally { + certFile.delete(); + keyFile.delete(); + } + } + + @Test + public void testBuildGrpcManagedChannelWithTlsAndCaCert() throws Exception { + // Generate test CA certificate + KeyPair caKeyPair = generateKeyPair(); + X509Certificate caCert = generateCertificate(caKeyPair); + + File caCertFile = File.createTempFile("test-ca-cert", ".pem"); + try { + writeCertificateToFile(caCert, caCertFile); + + var properties = new Properties(Map.of( + Properties.GRPC_TLS_CA_PATH.getName(), caCertFile.getAbsolutePath() + )); + + channel = NetworkUtils.buildGrpcManagedChannel(properties); + channels.add(channel); + String expectedAuthority = String.format("%s:%s", defaultSidecarIP, defaultGrpcPort); + Assertions.assertEquals(expectedAuthority, channel.authority()); + } finally { + caCertFile.delete(); + } + } + + @Test + public void testBuildGrpcManagedChannelWithTlsAndCaCertAndEndpoint() throws Exception { + // Generate test CA certificate + KeyPair caKeyPair = generateKeyPair(); + X509Certificate caCert = generateCertificate(caKeyPair); + + File caCertFile = File.createTempFile("test-ca-cert", ".pem"); + try { + writeCertificateToFile(caCert, caCertFile); + + var properties = new Properties(Map.of( + Properties.GRPC_TLS_CA_PATH.getName(), caCertFile.getAbsolutePath(), + Properties.GRPC_ENDPOINT.getName(), "https://example.com:443" + )); + + channel = NetworkUtils.buildGrpcManagedChannel(properties); + channels.add(channel); + Assertions.assertEquals("example.com:443", channel.authority()); + } finally { + caCertFile.delete(); + } + } + + @Test + public void testBuildGrpcManagedChannelWithInvalidCaCert() { + var properties = new Properties(Map.of( + Properties.GRPC_TLS_CA_PATH.getName(), "/nonexistent/ca.pem" + )); + + Assertions.assertThrows(DaprException.class, () -> { + NetworkUtils.buildGrpcManagedChannel(properties); + }); + } + + @Test + public void testBuildGrpcManagedChannelWithMtlsAndCaCert() throws Exception { + // Generate test certificates + KeyPair caKeyPair = generateKeyPair(); + X509Certificate caCert = generateCertificate(caKeyPair); + KeyPair clientKeyPair = generateKeyPair(); + X509Certificate clientCert = generateCertificate(clientKeyPair); + + File caCertFile = File.createTempFile("test-ca-cert", ".pem"); + File clientCertFile = File.createTempFile("test-client-cert", ".pem"); + File clientKeyFile = File.createTempFile("test-client-key", ".pem"); + try { + writeCertificateToFile(caCert, caCertFile); + writeCertificateToFile(clientCert, clientCertFile); + writePrivateKeyToFile(clientKeyPair, clientKeyFile); + + // Test mTLS with both client certs and CA cert + var properties = new Properties(Map.of( + Properties.GRPC_TLS_CA_PATH.getName(), caCertFile.getAbsolutePath(), + Properties.GRPC_TLS_CERT_PATH.getName(), clientCertFile.getAbsolutePath(), + Properties.GRPC_TLS_KEY_PATH.getName(), clientKeyFile.getAbsolutePath() + )); + + channel = NetworkUtils.buildGrpcManagedChannel(properties); + channels.add(channel); + String expectedAuthority = String.format("%s:%s", defaultSidecarIP, defaultGrpcPort); + Assertions.assertEquals(expectedAuthority, channel.authority()); + Assertions.assertFalse(channel.isTerminated(), "Channel should be active"); + } finally { + caCertFile.delete(); + clientCertFile.delete(); + clientKeyFile.delete(); + } + } + @Test public void testGrpcEndpointParsing() { testGrpcEndpointParsingScenario(":5000", "dns:///127.0.0.1:5000", false); @@ -146,4 +458,137 @@ private static void testGrpcEndpointParsingErrorScenario(String grpcEndpointEnvV // Expected } } + + @Test + public void testBuildGrpcManagedChannelWithCaCertAndUnixSocket() throws Exception { + // Skip test if Unix domain sockets are not supported + Assumptions.assumeTrue(System.getProperty("os.name").toLowerCase().contains("linux") || + System.getProperty("os.name").toLowerCase().contains("mac")); + + // Generate test CA certificate + KeyPair caKeyPair = generateKeyPair(); + X509Certificate caCert = generateCertificate(caKeyPair); + + File caCertFile = File.createTempFile("test-ca-cert", ".pem"); + try { + writeCertificateToFile(caCert, caCertFile); + + var properties = new Properties(Map.of( + Properties.GRPC_TLS_CA_PATH.getName(), caCertFile.getAbsolutePath(), + Properties.GRPC_ENDPOINT.getName(), "unix:/tmp/test.sock" + )); + + // For Unix sockets, we expect an exception if the platform doesn't support it + try { + channel = NetworkUtils.buildGrpcManagedChannel(properties); + channels.add(channel); + Assertions.assertNotNull(channel.authority(), "Channel authority should not be null"); + } catch (Exception e) { + // If we get here, Unix sockets are not supported + Assertions.assertTrue(e.getMessage().contains("DomainSocketAddress")); + } + } finally { + caCertFile.delete(); + } + } + + @Test + public void testBuildGrpcManagedChannelWithCaCertAndDnsAuthority() throws Exception { + // Generate test CA certificate + KeyPair caKeyPair = generateKeyPair(); + X509Certificate caCert = generateCertificate(caKeyPair); + + File caCertFile = File.createTempFile("test-ca-cert", ".pem"); + try { + writeCertificateToFile(caCert, caCertFile); + + var properties = new Properties(Map.of( + Properties.GRPC_TLS_CA_PATH.getName(), caCertFile.getAbsolutePath(), + Properties.GRPC_ENDPOINT.getName(), "dns://authority:53/example.com:443" + )); + + channel = NetworkUtils.buildGrpcManagedChannel(properties); + channels.add(channel); + Assertions.assertEquals("example.com:443", channel.authority()); + } finally { + caCertFile.delete(); + } + } + + @Test + public void testBuildGrpcManagedChannelWithInsecureTls() throws Exception { + // Test insecure TLS mode with a secure endpoint + var properties = new Properties(Map.of( + Properties.GRPC_TLS_INSECURE.getName(), "true", + Properties.GRPC_ENDPOINT.getName(), "dns:///example.com:443?tls=true" + )); + + channel = NetworkUtils.buildGrpcManagedChannel(properties); + channels.add(channel); + + // Verify the channel is created with the correct authority + Assertions.assertEquals("example.com:443", channel.authority()); + + // Verify the channel is active and using TLS (not plaintext) + Assertions.assertFalse(channel.isTerminated(), "Channel should be active"); + } + + @Test + public void testBuildGrpcManagedChannelWithInsecureTlsAndMtls() throws Exception { + // Generate test certificates + KeyPair caKeyPair = generateKeyPair(); + X509Certificate caCert = generateCertificate(caKeyPair); + KeyPair clientKeyPair = generateKeyPair(); + X509Certificate clientCert = generateCertificate(clientKeyPair); + + File caCertFile = File.createTempFile("test-ca-cert", ".pem"); + File clientCertFile = File.createTempFile("test-client-cert", ".pem"); + File clientKeyFile = File.createTempFile("test-client-key", ".pem"); + try { + writeCertificateToFile(caCert, caCertFile); + writeCertificateToFile(clientCert, clientCertFile); + writePrivateKeyToFile(clientKeyPair, clientKeyFile); + + // Test that insecure TLS still works with mTLS settings + // The client certs should be ignored since we're using InsecureTrustManagerFactory + var properties = new Properties(Map.of( + Properties.GRPC_TLS_INSECURE.getName(), "true", + Properties.GRPC_TLS_CA_PATH.getName(), caCertFile.getAbsolutePath(), + Properties.GRPC_TLS_CERT_PATH.getName(), clientCertFile.getAbsolutePath(), + Properties.GRPC_TLS_KEY_PATH.getName(), clientKeyFile.getAbsolutePath(), + Properties.GRPC_ENDPOINT.getName(), "dns:///example.com:443?tls=true" + )); + + channel = NetworkUtils.buildGrpcManagedChannel(properties); + channels.add(channel); + + // Verify the channel is created with the correct authority + Assertions.assertEquals("example.com:443", channel.authority()); + + // Verify the channel is active and using TLS (not plaintext) + Assertions.assertFalse(channel.isTerminated(), "Channel should be active"); + } finally { + caCertFile.delete(); + clientCertFile.delete(); + clientKeyFile.delete(); + } + } + + @Test + public void testBuildGrpcManagedChannelWithInsecureTlsAndCustomEndpoint() throws Exception { + // Test insecure TLS with a custom endpoint that would normally require TLS + var properties = new Properties(Map.of( + Properties.GRPC_TLS_INSECURE.getName(), "true", + Properties.GRPC_ENDPOINT.getName(), "dns://authority:53/example.com:443?tls=true" + )); + + channel = NetworkUtils.buildGrpcManagedChannel(properties); + channels.add(channel); + + // Verify the channel is created with the correct authority + Assertions.assertEquals("example.com:443", channel.authority()); + + // Verify the channel is active and using TLS (not plaintext) + Assertions.assertFalse(channel.isTerminated(), "Channel should be active"); + } } From dec2b073169a9222df2d6d4b05df5d962cd08b7f Mon Sep 17 00:00:00 2001 From: salaboy Date: Fri, 16 May 2025 09:48:30 +0100 Subject: [PATCH 02/12] spring boot workflow patterns initial version Signed-off-by: salaboy --- spring-boot-examples/pom.xml | 1 + spring-boot-examples/workflows/pom.xml | 68 +++++++++ .../wfp/WorkflowPatternsApplication.java | 27 ++++ .../wfp/WorkflowPatternsRestController.java | 134 +++++++++++++++++ .../examples/wfp/chain/ChainWorkflow.java | 36 +++++ .../wfp/chain/ToUpperCaseActivity.java | 38 +++++ .../examples/wfp/child/ChildWorkflow.java | 37 +++++ .../examples/wfp/child/ParentWorkflow.java | 37 +++++ .../examples/wfp/child/ReverseActivity.java | 37 +++++ .../wfp/continueasnew/CleanUpActivity.java | 51 +++++++ .../wfp/continueasnew/CleanUpLog.java | 22 +++ .../continueasnew/ContinueAsNewWorkflow.java | 58 ++++++++ .../wfp/externalevent/ApproveActivity.java | 41 +++++ .../examples/wfp/externalevent/Decision.java | 33 +++++ .../wfp/externalevent/DenyActivity.java | 41 +++++ .../externalevent/ExternalEventWorkflow.java | 43 ++++++ .../wfp/fanoutin/CountWordsActivity.java | 40 +++++ .../wfp/fanoutin/FanOutInWorkflow.java | 51 +++++++ .../examples/wfp/fanoutin/Result.java | 33 +++++ .../src/main/resources/application.properties | 4 + .../wfp/DaprTestContainersConfig.java | 45 ++++++ .../wfp/TestWorkflowPatternsApplication.java | 31 ++++ .../wfp/WorkflowPatternsAppTests.java | 140 ++++++++++++++++++ .../src/test/resources/application.properties | 3 + 24 files changed, 1051 insertions(+) create mode 100644 spring-boot-examples/workflows/pom.xml create mode 100644 spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/WorkflowPatternsApplication.java create mode 100644 spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/WorkflowPatternsRestController.java create mode 100644 spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/chain/ChainWorkflow.java create mode 100644 spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/chain/ToUpperCaseActivity.java create mode 100644 spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/child/ChildWorkflow.java create mode 100644 spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/child/ParentWorkflow.java create mode 100644 spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/child/ReverseActivity.java create mode 100644 spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/continueasnew/CleanUpActivity.java create mode 100644 spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/continueasnew/CleanUpLog.java create mode 100644 spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/continueasnew/ContinueAsNewWorkflow.java create mode 100644 spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/externalevent/ApproveActivity.java create mode 100644 spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/externalevent/Decision.java create mode 100644 spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/externalevent/DenyActivity.java create mode 100644 spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/externalevent/ExternalEventWorkflow.java create mode 100644 spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/fanoutin/CountWordsActivity.java create mode 100644 spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/fanoutin/FanOutInWorkflow.java create mode 100644 spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/fanoutin/Result.java create mode 100644 spring-boot-examples/workflows/src/main/resources/application.properties create mode 100644 spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/DaprTestContainersConfig.java create mode 100644 spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/TestWorkflowPatternsApplication.java create mode 100644 spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/WorkflowPatternsAppTests.java create mode 100644 spring-boot-examples/workflows/src/test/resources/application.properties diff --git a/spring-boot-examples/pom.xml b/spring-boot-examples/pom.xml index 84ebb6c510..0b333b703a 100644 --- a/spring-boot-examples/pom.xml +++ b/spring-boot-examples/pom.xml @@ -21,6 +21,7 @@ producer-app consumer-app + workflows diff --git a/spring-boot-examples/workflows/pom.xml b/spring-boot-examples/workflows/pom.xml new file mode 100644 index 0000000000..4711b799be --- /dev/null +++ b/spring-boot-examples/workflows/pom.xml @@ -0,0 +1,68 @@ + + + 4.0.0 + + + io.dapr + spring-boot-examples + 0.16.0-SNAPSHOT + + + workflows + workflows + Spring Boot, Testcontainers and Dapr Integration Examples :: Workflows + + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-test + + + io.dapr.spring + dapr-spring-boot-starter + + + io.dapr.spring + dapr-spring-boot-starter-test + test + + + io.rest-assured + rest-assured + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-site-plugin + + true + + + + org.apache.maven.plugins + maven-checkstyle-plugin + + + true + + + + + diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/WorkflowPatternsApplication.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/WorkflowPatternsApplication.java new file mode 100644 index 0000000000..378295a2ff --- /dev/null +++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/WorkflowPatternsApplication.java @@ -0,0 +1,27 @@ +/* + * Copyright 2025 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.springboot.examples.wfp; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + + +@SpringBootApplication +public class WorkflowPatternsApplication { + + public static void main(String[] args) { + SpringApplication.run(WorkflowPatternsApplication.class, args); + } + +} diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/WorkflowPatternsRestController.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/WorkflowPatternsRestController.java new file mode 100644 index 0000000000..da503d9304 --- /dev/null +++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/WorkflowPatternsRestController.java @@ -0,0 +1,134 @@ +/* + * Copyright 2025 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.springboot.examples.wfp; + +import io.dapr.spring.workflows.config.EnableDaprWorkflows; +import io.dapr.springboot.examples.wfp.chain.ChainWorkflow; +import io.dapr.springboot.examples.wfp.child.ParentWorkflow; +import io.dapr.springboot.examples.wfp.continueasnew.CleanUpLog; +import io.dapr.springboot.examples.wfp.continueasnew.ContinueAsNewWorkflow; +import io.dapr.springboot.examples.wfp.externalevent.Decision; +import io.dapr.springboot.examples.wfp.externalevent.ExternalEventWorkflow; +import io.dapr.springboot.examples.wfp.fanoutin.FanOutInWorkflow; +import io.dapr.springboot.examples.wfp.fanoutin.Result; +import io.dapr.workflows.client.DaprWorkflowClient; +import io.dapr.workflows.client.WorkflowInstanceStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.time.Duration; +import java.util.List; +import java.util.concurrent.TimeoutException; + +@RestController +@EnableDaprWorkflows +public class WorkflowPatternsRestController { + + private final Logger logger = LoggerFactory.getLogger(WorkflowPatternsRestController.class); + + @Autowired + private DaprWorkflowClient daprWorkflowClient; + + @Bean + public CleanUpLog cleanUpLog(){ + return new CleanUpLog(); + } + + /** + * Run Chain Demo Workflow + * @return the output of the ChainWorkflow execution + */ + @PostMapping("wfp/chain") + public String chain() throws TimeoutException { + String instanceId = daprWorkflowClient.scheduleNewWorkflow(ChainWorkflow.class); + logger.info("Workflow instance " + instanceId + " started"); + return daprWorkflowClient + .waitForInstanceCompletion(instanceId, Duration.ofSeconds(2), true) + .readOutputAs(String.class); + } + + + /** + * Run Child Demo Workflow + * @return confirmation that the workflow instance was created for the workflow pattern child + */ + @PostMapping("wfp/child") + public String child() throws TimeoutException { + String instanceId = daprWorkflowClient.scheduleNewWorkflow(ParentWorkflow.class); + logger.info("Workflow instance " + instanceId + " started"); + return daprWorkflowClient + .waitForInstanceCompletion(instanceId, Duration.ofSeconds(2), true) + .readOutputAs(String.class); + } + + + /** + * Run Fan Out/in Demo Workflow + * @return confirmation that the workflow instance was created for the workflow pattern faninout + */ + @PostMapping("wfp/fanoutin") + public Result faninout(@RequestBody List listOfStrings) throws TimeoutException { + + String instanceId = daprWorkflowClient.scheduleNewWorkflow(FanOutInWorkflow.class, listOfStrings); + logger.info("Workflow instance " + instanceId + " started"); + + // Block until the orchestration completes. Then print the final status, which includes the output. + WorkflowInstanceStatus workflowInstanceStatus = daprWorkflowClient.waitForInstanceCompletion( + instanceId, + Duration.ofSeconds(30), + true); + logger.info("workflow instance with ID: %s completed with result: %s%n", instanceId, + workflowInstanceStatus.readOutputAs(Result.class)); + return workflowInstanceStatus.readOutputAs(Result.class); + } + + /** + * Run External Event Workflow Pattern + * @return confirmation that the workflow instance was created for the workflow pattern externalevent + */ + @PostMapping("wfp/externalevent") + public String externalevent() { + String instanceId = daprWorkflowClient.scheduleNewWorkflow(ExternalEventWorkflow.class); + logger.info("Workflow instance " + instanceId + " started"); + return instanceId; + } + + @PostMapping("wfp/externalevent-continue") + public Decision externaleventContinue(@RequestParam("instanceId") String instanceId, @RequestParam("decision") Boolean decision) + throws TimeoutException { + logger.info("Workflow instance " + instanceId + " continue"); + daprWorkflowClient.raiseEvent(instanceId, "Approval", decision); + WorkflowInstanceStatus workflowInstanceStatus = daprWorkflowClient + .waitForInstanceCompletion(instanceId, null, true); + return workflowInstanceStatus.readOutputAs(Decision.class); + } + + @PostMapping("wfp/continueasnew") + public CleanUpLog continueasnew() + throws TimeoutException { + String instanceId = daprWorkflowClient.scheduleNewWorkflow(ContinueAsNewWorkflow.class); + logger.info("Workflow instance " + instanceId + " started"); + + WorkflowInstanceStatus workflowInstanceStatus = daprWorkflowClient.waitForInstanceCompletion(instanceId, null, true); + System.out.printf("workflow instance with ID: %s completed.", instanceId); + return workflowInstanceStatus.readOutputAs(CleanUpLog.class); + } + +} \ No newline at end of file diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/chain/ChainWorkflow.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/chain/ChainWorkflow.java new file mode 100644 index 0000000000..d1967247fe --- /dev/null +++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/chain/ChainWorkflow.java @@ -0,0 +1,36 @@ +/* + * Copyright 2023 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.springboot.examples.wfp.chain; + +import io.dapr.workflows.Workflow; +import io.dapr.workflows.WorkflowStub; +import org.springframework.stereotype.Component; + +@Component +public class ChainWorkflow implements Workflow { + @Override + public WorkflowStub create() { + return ctx -> { + ctx.getLogger().info("Starting Workflow: " + ctx.getName()); + + String result = ""; + result += ctx.callActivity(ToUpperCaseActivity.class.getName(), "Tokyo", String.class).await() + ", "; + result += ctx.callActivity(ToUpperCaseActivity.class.getName(), "London", String.class).await() + ", "; + result += ctx.callActivity(ToUpperCaseActivity.class.getName(), "Seattle", String.class).await(); + + ctx.getLogger().info("Workflow finished with result: " + result); + ctx.complete(result); + }; + } +} diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/chain/ToUpperCaseActivity.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/chain/ToUpperCaseActivity.java new file mode 100644 index 0000000000..6280bcf71e --- /dev/null +++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/chain/ToUpperCaseActivity.java @@ -0,0 +1,38 @@ +/* + * Copyright 2023 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.springboot.examples.wfp.chain; + +import io.dapr.workflows.WorkflowActivity; +import io.dapr.workflows.WorkflowActivityContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +@Component +public class ToUpperCaseActivity implements WorkflowActivity { + + @Override + public Object run(WorkflowActivityContext ctx) { + Logger logger = LoggerFactory.getLogger(ToUpperCaseActivity.class); + logger.info("Starting Activity: " + ctx.getName()); + + var message = ctx.getInput(String.class); + var newMessage = message.toUpperCase(); + + logger.info("Message Received from input: " + message); + logger.info("Sending message to output: " + newMessage); + + return newMessage; + } +} \ No newline at end of file diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/child/ChildWorkflow.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/child/ChildWorkflow.java new file mode 100644 index 0000000000..3267337b1e --- /dev/null +++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/child/ChildWorkflow.java @@ -0,0 +1,37 @@ +/* + * Copyright 2023 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.springboot.examples.wfp.child; + +import io.dapr.workflows.Workflow; +import io.dapr.workflows.WorkflowStub; +import org.springframework.stereotype.Component; + +@Component +public class ChildWorkflow implements Workflow { + @Override + public WorkflowStub create() { + return ctx -> { + ctx.getLogger().info("Starting ChildWorkflow: " + ctx.getName()); + + var childWorkflowInput = ctx.getInput(String.class); + ctx.getLogger().info("ChildWorkflow received input: " + childWorkflowInput); + + ctx.getLogger().info("ChildWorkflow is calling Activity: " + ReverseActivity.class.getName()); + String result = ctx.callActivity(ReverseActivity.class.getName(), childWorkflowInput, String.class).await(); + + ctx.getLogger().info("ChildWorkflow finished with: " + result); + ctx.complete(result); + }; + } +} diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/child/ParentWorkflow.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/child/ParentWorkflow.java new file mode 100644 index 0000000000..d5d58d3c91 --- /dev/null +++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/child/ParentWorkflow.java @@ -0,0 +1,37 @@ +/* + * Copyright 2023 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.springboot.examples.wfp.child; + +import io.dapr.workflows.Workflow; +import io.dapr.workflows.WorkflowStub; +import org.springframework.stereotype.Component; + +@Component +public class ParentWorkflow implements Workflow { + @Override + public WorkflowStub create() { + return ctx -> { + ctx.getLogger().info("Starting Workflow: " + ctx.getName()); + + var childWorkflowInput = "Hello Dapr Workflow!"; + ctx.getLogger().info("calling childworkflow with input: " + childWorkflowInput); + + var childWorkflowOutput = + ctx.callChildWorkflow(ChildWorkflow.class.getName(), childWorkflowInput, String.class).await(); + + ctx.getLogger().info("childworkflow finished with: " + childWorkflowOutput); + ctx.complete(childWorkflowOutput); + }; + } +} diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/child/ReverseActivity.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/child/ReverseActivity.java new file mode 100644 index 0000000000..af483cdfb6 --- /dev/null +++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/child/ReverseActivity.java @@ -0,0 +1,37 @@ +/* + * Copyright 2023 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.springboot.examples.wfp.child; + +import io.dapr.workflows.WorkflowActivity; +import io.dapr.workflows.WorkflowActivityContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +@Component +public class ReverseActivity implements WorkflowActivity { + @Override + public Object run(WorkflowActivityContext ctx) { + Logger logger = LoggerFactory.getLogger(ReverseActivity.class); + logger.info("Starting Activity: " + ctx.getName()); + + var message = ctx.getInput(String.class); + var newMessage = new StringBuilder(message).reverse().toString(); + + logger.info("Message Received from input: " + message); + logger.info("Sending message to output: " + newMessage); + + return newMessage; + } +} diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/continueasnew/CleanUpActivity.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/continueasnew/CleanUpActivity.java new file mode 100644 index 0000000000..2fcc8f01d4 --- /dev/null +++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/continueasnew/CleanUpActivity.java @@ -0,0 +1,51 @@ +/* + * Copyright 2023 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.springboot.examples.wfp.continueasnew; + +import io.dapr.workflows.WorkflowActivity; +import io.dapr.workflows.WorkflowActivityContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.util.concurrent.TimeUnit; + +@Component +public class CleanUpActivity implements WorkflowActivity { + + @Autowired + private CleanUpLog cleanUpLog; + + @Override + public Object run(WorkflowActivityContext ctx) { + Logger logger = LoggerFactory.getLogger(CleanUpActivity.class); + logger.info("Starting Activity: " + ctx.getName()); + + LocalDateTime now = LocalDateTime.now(); + String cleanUpTimeString = now.getHour() + ":" + now.getMinute() + ":" + now.getSecond(); + cleanUpLog.getCleanUpTimes().add(cleanUpTimeString); + logger.info("start clean up work, it may take few seconds to finish... Time:" + cleanUpTimeString); + + //Sleeping for 2 seconds to simulate long running operation + try { + TimeUnit.SECONDS.sleep(2); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + return "clean up finish."; + } +} diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/continueasnew/CleanUpLog.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/continueasnew/CleanUpLog.java new file mode 100644 index 0000000000..34ceb0d8e5 --- /dev/null +++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/continueasnew/CleanUpLog.java @@ -0,0 +1,22 @@ +package io.dapr.springboot.examples.wfp.continueasnew; + +import java.util.ArrayList; +import java.util.List; + +public class CleanUpLog { + private List cleanUpTimes = new ArrayList<>(); + + public CleanUpLog() { + } + + public List getCleanUpTimes() { + return cleanUpTimes; + } + + @Override + public String toString() { + return "CleanUpLog{" + + "cleanUpTimes=" + cleanUpTimes + + '}'; + } +} diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/continueasnew/ContinueAsNewWorkflow.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/continueasnew/ContinueAsNewWorkflow.java new file mode 100644 index 0000000000..872d9ae268 --- /dev/null +++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/continueasnew/ContinueAsNewWorkflow.java @@ -0,0 +1,58 @@ +/* + * Copyright 2023 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.springboot.examples.wfp.continueasnew; + +import io.dapr.workflows.Workflow; +import io.dapr.workflows.WorkflowStub; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.time.Duration; + +@Component +public class ContinueAsNewWorkflow implements Workflow { + /* + Compared with a CRON schedule, this periodic workflow example will never overlap. + For example, a CRON schedule that executes a cleanup every hour will execute it at 1:00, 2:00, 3:00 etc. + and could potentially run into overlap issues if the cleanup takes longer than an hour. + In this example, however, if the cleanup takes 30 minutes, and we create a timer for 1 hour between cleanups, + then it will be scheduled at 1:00, 2:30, 4:00, etc. and there is no chance of overlap. + */ + + @Autowired + private CleanUpLog cleanUpLog; + + @Override + public WorkflowStub create() { + return ctx -> { + ctx.getLogger().info("Starting Workflow: " + ctx.getName()); + + ctx.getLogger().info("call CleanUpActivity to do the clean up"); + ctx.callActivity(CleanUpActivity.class.getName(), cleanUpLog).await(); + ctx.getLogger().info("CleanUpActivity finished"); + + ctx.getLogger().info("wait 5 seconds for next clean up"); + ctx.createTimer(Duration.ofSeconds(3)).await(); + + if(cleanUpLog.getCleanUpTimes().size() < 5) { + // continue the workflow. + ctx.getLogger().info("Let's do more cleaning."); + ctx.continueAsNew(null); + } else{ + ctx.getLogger().info("We did enough cleaning"); + ctx.complete(cleanUpLog); + } + }; + } +} diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/externalevent/ApproveActivity.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/externalevent/ApproveActivity.java new file mode 100644 index 0000000000..e8ea8f7c86 --- /dev/null +++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/externalevent/ApproveActivity.java @@ -0,0 +1,41 @@ +/* + * Copyright 2023 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.springboot.examples.wfp.externalevent; + +import io.dapr.workflows.WorkflowActivity; +import io.dapr.workflows.WorkflowActivityContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +@Component +public class ApproveActivity implements WorkflowActivity { + @Override + public Object run(WorkflowActivityContext ctx) { + Logger logger = LoggerFactory.getLogger(ApproveActivity.class); + logger.info("Starting Activity: " + ctx.getName()); + + logger.info("Running approval activity..."); + //Sleeping for 5 seconds to simulate long running operation + try { + TimeUnit.SECONDS.sleep(5); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + return new Decision(true); + } +} diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/externalevent/Decision.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/externalevent/Decision.java new file mode 100644 index 0000000000..2f8d2fee91 --- /dev/null +++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/externalevent/Decision.java @@ -0,0 +1,33 @@ +/* + * Copyright 2025 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.springboot.examples.wfp.externalevent; + +public class Decision { + private Boolean approved; + + public Decision() { + } + + public Decision(Boolean approved) { + this.approved = approved; + } + + public Boolean getApproved() { + return approved; + } + + public void setApproved(Boolean approved) { + this.approved = approved; + } +} diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/externalevent/DenyActivity.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/externalevent/DenyActivity.java new file mode 100644 index 0000000000..bd40793760 --- /dev/null +++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/externalevent/DenyActivity.java @@ -0,0 +1,41 @@ +/* + * Copyright 2023 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.springboot.examples.wfp.externalevent; + +import io.dapr.workflows.WorkflowActivity; +import io.dapr.workflows.WorkflowActivityContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +@Component +public class DenyActivity implements WorkflowActivity { + @Override + public Object run(WorkflowActivityContext ctx) { + Logger logger = LoggerFactory.getLogger(DenyActivity.class); + logger.info("Starting Activity: " + ctx.getName()); + + logger.info("Running denied activity..."); + //Sleeping for 5 seconds to simulate long running operation + try { + TimeUnit.SECONDS.sleep(5); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + return new Decision(false); + } +} diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/externalevent/ExternalEventWorkflow.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/externalevent/ExternalEventWorkflow.java new file mode 100644 index 0000000000..952d5eede0 --- /dev/null +++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/externalevent/ExternalEventWorkflow.java @@ -0,0 +1,43 @@ +/* + * Copyright 2023 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.springboot.examples.wfp.externalevent; + +import io.dapr.workflows.Workflow; +import io.dapr.workflows.WorkflowStub; +import org.springframework.stereotype.Component; + +@Component +public class ExternalEventWorkflow implements Workflow { + @Override + public WorkflowStub create() { + return ctx -> { + ctx.getLogger().info("Starting Workflow: " + ctx.getName()); + + ctx.getLogger().info("Waiting for approval..."); + Boolean approved = ctx.waitForExternalEvent("Approval", boolean.class).await(); + Decision decision = null; + if (approved) { + ctx.getLogger().info("approval granted - do the approved action"); + decision = ctx.callActivity(ApproveActivity.class.getName(), Decision.class).await(); + + ctx.getLogger().info("approval-activity finished"); + } else { + ctx.getLogger().info("approval denied - send a notification"); + decision = ctx.callActivity(DenyActivity.class.getName(), Decision.class).await(); + ctx.getLogger().info("denied-activity finished"); + } + ctx.complete(decision); + }; + } +} diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/fanoutin/CountWordsActivity.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/fanoutin/CountWordsActivity.java new file mode 100644 index 0000000000..5863d56924 --- /dev/null +++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/fanoutin/CountWordsActivity.java @@ -0,0 +1,40 @@ +/* + * Copyright 2023 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.springboot.examples.wfp.fanoutin; + +import io.dapr.workflows.WorkflowActivity; +import io.dapr.workflows.WorkflowActivityContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import java.util.StringTokenizer; + +@Component +public class CountWordsActivity implements WorkflowActivity { + @Override + public Object run(WorkflowActivityContext ctx) { + Logger logger = LoggerFactory.getLogger(CountWordsActivity.class); + logger.info("Starting Activity: {}", ctx.getName()); + + String input = ctx.getInput(String.class); + StringTokenizer tokenizer = new StringTokenizer(input); + int result = tokenizer.countTokens(); + + logger.info("Activity returned: {}.", result); + logger.info("Activity finished"); + + return result; + } +} \ No newline at end of file diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/fanoutin/FanOutInWorkflow.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/fanoutin/FanOutInWorkflow.java new file mode 100644 index 0000000000..09f3edc571 --- /dev/null +++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/fanoutin/FanOutInWorkflow.java @@ -0,0 +1,51 @@ +/* + * Copyright 2023 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.springboot.examples.wfp.fanoutin; + +import io.dapr.durabletask.Task; +import io.dapr.workflows.Workflow; +import io.dapr.workflows.WorkflowStub; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.stream.Collectors; + +@Component +public class FanOutInWorkflow implements Workflow { + @Override + public WorkflowStub create() { + return ctx -> { + + ctx.getLogger().info("Starting Workflow: " + ctx.getName()); + + + // The input is a list of objects that need to be operated on. + // In this example, inputs are expected to be strings. + List inputs = ctx.getInput(List.class); + + // Fan-out to multiple concurrent activity invocations, each of which does a word count. + List> tasks = inputs.stream() + .map(input -> ctx.callActivity(CountWordsActivity.class.getName(), input.toString(), Integer.class)) + .collect(Collectors.toList()); + + // Fan-in to get the total word count from all the individual activity results. + List allWordCountResults = ctx.allOf(tasks).await(); + int totalWordCount = allWordCountResults.stream().mapToInt(Integer::intValue).sum(); + + ctx.getLogger().info("Workflow finished with result: " + totalWordCount); + // Save the final result as the orchestration output. + ctx.complete(new Result(totalWordCount)); + }; + } +} \ No newline at end of file diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/fanoutin/Result.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/fanoutin/Result.java new file mode 100644 index 0000000000..0154840147 --- /dev/null +++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/fanoutin/Result.java @@ -0,0 +1,33 @@ +/* + * Copyright 2025 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.springboot.examples.wfp.fanoutin; + +public class Result { + private Integer wordCount; + + public Result() { + } + + public Result(Integer wordCount) { + this.wordCount = wordCount; + } + + public Integer getWordCount() { + return wordCount; + } + + public void setWordCount(Integer wordCount) { + this.wordCount = wordCount; + } +}; diff --git a/spring-boot-examples/workflows/src/main/resources/application.properties b/spring-boot-examples/workflows/src/main/resources/application.properties new file mode 100644 index 0000000000..1498965c7d --- /dev/null +++ b/spring-boot-examples/workflows/src/main/resources/application.properties @@ -0,0 +1,4 @@ +spring.application.name=producer-app +dapr.pubsub.name=pubsub +dapr.statestore.name=kvstore +dapr.statestore.binding=kvbinding diff --git a/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/DaprTestContainersConfig.java b/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/DaprTestContainersConfig.java new file mode 100644 index 0000000000..a182af1c44 --- /dev/null +++ b/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/DaprTestContainersConfig.java @@ -0,0 +1,45 @@ +/* + * Copyright 2025 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.springboot.examples.wfp; + +import io.dapr.testcontainers.Component; +import io.dapr.testcontainers.DaprContainer; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.context.annotation.Bean; + +import java.util.Collections; + +import static io.dapr.testcontainers.DaprContainerConstants.DAPR_RUNTIME_IMAGE_TAG; + +@TestConfiguration(proxyBeanMethods = false) +public class DaprTestContainersConfig { + + @Bean + @ServiceConnection + public DaprContainer daprContainer() { + + return new DaprContainer(DAPR_RUNTIME_IMAGE_TAG) + .withAppName("workflow-patterns-app") + .withComponent(new Component("kvstore", "state.in-memory", "v1", Collections.singletonMap("actorStateStore", String.valueOf(true)))) +// .withDaprLogLevel(DaprLogLevel.DEBUG) +// .withLogConsumer(outputFrame -> System.out.println(outputFrame.getUtf8String())) + .withAppPort(8080) + .withAppHealthCheckPath("/actuator/health") + .withAppChannelAddress("host.testcontainers.internal"); + } + + + +} diff --git a/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/TestWorkflowPatternsApplication.java b/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/TestWorkflowPatternsApplication.java new file mode 100644 index 0000000000..8459f8d50d --- /dev/null +++ b/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/TestWorkflowPatternsApplication.java @@ -0,0 +1,31 @@ +/* + * Copyright 2025 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.springboot.examples.wfp; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + + +@SpringBootApplication +public class TestWorkflowPatternsApplication { + + public static void main(String[] args) { + + SpringApplication.from(WorkflowPatternsApplication::main) + .with(DaprTestContainersConfig.class) + .run(args); + org.testcontainers.Testcontainers.exposeHostPorts(8080); + } + +} diff --git a/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/WorkflowPatternsAppTests.java b/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/WorkflowPatternsAppTests.java new file mode 100644 index 0000000000..6643e78fe9 --- /dev/null +++ b/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/WorkflowPatternsAppTests.java @@ -0,0 +1,140 @@ +/* + * Copyright 2025 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.springboot.examples.wfp; + +import io.dapr.client.DaprClient; +import io.dapr.springboot.DaprAutoConfiguration; +import io.dapr.springboot.examples.wfp.continueasnew.CleanUpLog; +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.Arrays; +import java.util.List; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@SpringBootTest(classes = {TestWorkflowPatternsApplication.class, DaprTestContainersConfig.class, + DaprAutoConfiguration.class, }, + webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) +class WorkflowPatternsAppTests { + + @Autowired + private DaprClient daprClient; + + @BeforeEach + void setUp() { + RestAssured.baseURI = "http://localhost:" + 8080; + org.testcontainers.Testcontainers.exposeHostPorts(8080); + } + + + @Test + void testChainWorkflow() { + given().contentType(ContentType.JSON) + .body("") + .when() + .post("/wfp/chain") + .then() + .statusCode(200).body(containsString("TOKYO, LONDON, SEATTLE")); + } + + @Test + void testChildWorkflow() { + given().contentType(ContentType.JSON) + .body("") + .when() + .post("/wfp/child") + .then() + .statusCode(200).body(containsString("!wolfkroW rpaD olleH")); + } + + @Test + void testFanOutIn() { + List listOfStrings = Arrays.asList( + "Hello, world!", + "The quick brown fox jumps over the lazy dog.", + "If a tree falls in the forest and there is no one there to hear it, does it make a sound?", + "The greatest glory in living lies not in never falling, but in rising every time we fall.", + "Always remember that you are absolutely unique. Just like everyone else."); + + given().contentType(ContentType.JSON) + .body(listOfStrings) + .when() + .post("/wfp/fanoutin") + .then() + .statusCode(200).body("wordCount",equalTo(60)); + } + + @Test + void testExternalEventApprove() { + + String instanceId = given() + .when() + .post("/wfp/externalevent") + .then() + .statusCode(200).extract().asString(); + + + + given() + .queryParam("instanceId", instanceId) + .queryParam("decision", true) + .when() + .post("/wfp/externalevent-continue") + .then() + .statusCode(200).body("approved", equalTo(true)); + } + + @Test + void testExternalEventDeny() { + + String instanceId = given() + .when() + .post("/wfp/externalevent") + .then() + .statusCode(200).extract().asString(); + + + + given() + .queryParam("instanceId", instanceId) + .queryParam("decision", false) + .when() + .post("/wfp/externalevent-continue") + .then() + .statusCode(200).body("approved", equalTo(false)); + } + + + @Test + void testContinueAsNew() { + //This call blocks until all the clean up activities are executed + CleanUpLog cleanUpLog = given().contentType(ContentType.JSON) + .body("") + .when() + .post("/wfp/continueasnew") + .then() + .statusCode(200).extract().as(CleanUpLog.class); + + assertEquals(5, cleanUpLog.getCleanUpTimes().size()); + } + +} diff --git a/spring-boot-examples/workflows/src/test/resources/application.properties b/spring-boot-examples/workflows/src/test/resources/application.properties new file mode 100644 index 0000000000..2429abb69a --- /dev/null +++ b/spring-boot-examples/workflows/src/test/resources/application.properties @@ -0,0 +1,3 @@ +dapr.statestore.name=kvstore +dapr.statestore.binding=kvbinding +dapr.pubsub.name=pubsub From c8c033064f65c087700421c05a8c91e6ec2eac56 Mon Sep 17 00:00:00 2001 From: salaboy Date: Fri, 16 May 2025 10:08:14 +0100 Subject: [PATCH 03/12] adding README for workflows Signed-off-by: salaboy --- spring-boot-examples/workflows/README.md | 126 ++++++++++++++++++ .../src/test/resources/application.properties | 3 - 2 files changed, 126 insertions(+), 3 deletions(-) create mode 100644 spring-boot-examples/workflows/README.md diff --git a/spring-boot-examples/workflows/README.md b/spring-boot-examples/workflows/README.md new file mode 100644 index 0000000000..ddd9491aaf --- /dev/null +++ b/spring-boot-examples/workflows/README.md @@ -0,0 +1,126 @@ +# Dapr Spring Boot Workflow Examples + +This application allows you to run different workflow patterns including: +- Chained Activities +- Parent/Child Workflows +- Continue workflow by sending External Events +- Fan Out/In activities for parallel execution + +## Running these examples from source code + +To run these examples you will need: +- Java SDK +- Maven +- Docker or a container runtime such as Podman + +From the `spring-boot-examples/workflows` directory you can start the service using the test configuration that uses +[Testcontainers](https://testcontainers.com) to boostrap [Dapr](https://dapr.io) by running the following command: + + + + +```sh +cd workflows/ +../../mvnw spring-boot:test-run +``` + + + +Once the application is running you can trigger the different patterns by sending the following requests: + +### Chaining Activities Workflow example + +The `io.dapr.springboot.examples.wfp.chain.ChainWorkflow` executes three chained activities. For this example the +`ToUpperCaseActivity.java` is used to transform to upper case three strings from an array. + +```mermaid +graph LR + SW((Start + Workflow)) + A1[Activity1] + A2[Activity2] + A3[Activity3] + EW((End + Workflow)) + SW --> A1 + A1 --> A2 + A2 --> A3 + A3 --> EW +``` + + + + +```sh +curl -X POST localhost:8080/wfp/chain -H 'Content-Type: application/json' +``` + + + + +As result from executing the request you should see: + +```bash +TOKYO, LONDON, SEATTLE +``` + +In the application output you should see the workflow activities being executed. + +```bash +2025-05-16T09:59:22.176+01:00 INFO 8360 --- [pool-3-thread-1] io.dapr.workflows.WorkflowContext : Starting Workflow: io.dapr.springboot.examples.wfp.chain.ChainWorkflow +2025-05-16T09:59:22.194+01:00 INFO 8360 --- [nio-8080-exec-2] i.d.s.e.w.WorkflowPatternsRestController : Workflow instance 7625b4af-8c04-408a-93dc-bad753466e43 started +2025-05-16T09:59:22.195+01:00 INFO 8360 --- [pool-3-thread-2] i.d.s.e.wfp.chain.ToUpperCaseActivity : Starting Activity: io.dapr.springboot.examples.wfp.chain.ToUpperCaseActivity +2025-05-16T09:59:22.196+01:00 INFO 8360 --- [pool-3-thread-2] i.d.s.e.wfp.chain.ToUpperCaseActivity : Message Received from input: Tokyo +2025-05-16T09:59:22.197+01:00 INFO 8360 --- [pool-3-thread-2] i.d.s.e.wfp.chain.ToUpperCaseActivity : Sending message to output: TOKYO +2025-05-16T09:59:22.210+01:00 INFO 8360 --- [pool-3-thread-1] i.d.s.e.wfp.chain.ToUpperCaseActivity : Starting Activity: io.dapr.springboot.examples.wfp.chain.ToUpperCaseActivity +2025-05-16T09:59:22.210+01:00 INFO 8360 --- [pool-3-thread-1] i.d.s.e.wfp.chain.ToUpperCaseActivity : Message Received from input: London +2025-05-16T09:59:22.210+01:00 INFO 8360 --- [pool-3-thread-1] i.d.s.e.wfp.chain.ToUpperCaseActivity : Sending message to output: LONDON +2025-05-16T09:59:22.216+01:00 INFO 8360 --- [pool-3-thread-3] i.d.s.e.wfp.chain.ToUpperCaseActivity : Starting Activity: io.dapr.springboot.examples.wfp.chain.ToUpperCaseActivity +2025-05-16T09:59:22.216+01:00 INFO 8360 --- [pool-3-thread-3] i.d.s.e.wfp.chain.ToUpperCaseActivity : Message Received from input: Seattle +2025-05-16T09:59:22.216+01:00 INFO 8360 --- [pool-3-thread-3] i.d.s.e.wfp.chain.ToUpperCaseActivity : Sending message to output: SEATTLE +2025-05-16T09:59:22.219+01:00 INFO 8360 --- [pool-3-thread-1] io.dapr.workflows.WorkflowContext : Workflow finished with result: TOKYO, LONDON, SEATTLE +``` + +### Parent / Child Workflows example + + +```mermaid +graph LR + SW((Start + Workflow)) + subgraph for each word in the input + GWL[Call child workflow] + end + ALL[Wait until all tasks + are completed] + EW((End + Workflow)) + SW --> GWL + GWL --> ALL + ALL --> EW +``` + + +## Testing workflow executions + +Workflow execution can be tested using Testcontainers and you can find all the tests for the patterns covered in this +application [here](test/java/io/dapr/springboot/examples/wfp/TestWorkflowPatternsApplication.java). \ No newline at end of file diff --git a/spring-boot-examples/workflows/src/test/resources/application.properties b/spring-boot-examples/workflows/src/test/resources/application.properties index 2429abb69a..e69de29bb2 100644 --- a/spring-boot-examples/workflows/src/test/resources/application.properties +++ b/spring-boot-examples/workflows/src/test/resources/application.properties @@ -1,3 +0,0 @@ -dapr.statestore.name=kvstore -dapr.statestore.binding=kvbinding -dapr.pubsub.name=pubsub From 4c37b0b81cafb3aa36445946a35f68263ffc3212 Mon Sep 17 00:00:00 2001 From: salaboy Date: Fri, 16 May 2025 10:31:10 +0100 Subject: [PATCH 04/12] adding child example Signed-off-by: salaboy --- spring-boot-examples/workflows/README.md | 81 ++++++++++++++++++++++-- 1 file changed, 75 insertions(+), 6 deletions(-) diff --git a/spring-boot-examples/workflows/README.md b/spring-boot-examples/workflows/README.md index ddd9491aaf..eb9b6a305b 100644 --- a/spring-boot-examples/workflows/README.md +++ b/spring-boot-examples/workflows/README.md @@ -59,17 +59,19 @@ graph LR ``` +To start the workflow with the three chained activities you can run: + ```sh curl -X POST localhost:8080/wfp/chain -H 'Content-Type: application/json' ``` @@ -102,6 +104,9 @@ In the application output you should see the workflow activities being executed. ### Parent / Child Workflows example +In this example we start a Parent workflow that calls a child workflow that execute one activity that reverse the . + +The Parent workflow looks like this: ```mermaid graph LR @@ -110,15 +115,79 @@ graph LR subgraph for each word in the input GWL[Call child workflow] end - ALL[Wait until all tasks - are completed] EW((End Workflow)) SW --> GWL - GWL --> ALL - ALL --> EW + GWL --> EW ``` +The Child workflow looks like this: + +```mermaid +graph LR + SW((Start + Workflow)) + A1[Activity1] + EW((End + Workflow)) + SW --> A1 + A1 --> EW +``` + +To start the parent workflow you can run: + + + + + +To start the workflow with the three chained activities you can run: + +```sh +curl -X POST localhost:8080/wfp/child -H 'Content-Type: application/json' +``` + + + + +As result from executing the request you should see: + +```bash +!wolfkroW rpaD olleH +``` + +In the application output you should see the workflow activities being executed. + +```bash +2025-05-16T10:27:01.845+01:00 INFO 9855 --- [pool-3-thread-1] io.dapr.workflows.WorkflowContext : Starting Workflow: io.dapr.springboot.examples.wfp.child.ParentWorkflow +2025-05-16T10:27:01.845+01:00 INFO 9855 --- [pool-3-thread-1] io.dapr.workflows.WorkflowContext : calling childworkflow with input: Hello Dapr Workflow! +2025-05-16T10:27:01.865+01:00 INFO 9855 --- [nio-8080-exec-1] i.d.s.e.w.WorkflowPatternsRestController : Workflow instance f3ec9566-a0fc-4d28-8912-3f3ded3cd8a9 started +2025-05-16T10:27:01.866+01:00 INFO 9855 --- [pool-3-thread-2] io.dapr.workflows.WorkflowContext : Starting ChildWorkflow: io.dapr.springboot.examples.wfp.child.ChildWorkflow +2025-05-16T10:27:01.868+01:00 INFO 9855 --- [pool-3-thread-2] io.dapr.workflows.WorkflowContext : ChildWorkflow received input: Hello Dapr Workflow! +2025-05-16T10:27:01.868+01:00 INFO 9855 --- [pool-3-thread-2] io.dapr.workflows.WorkflowContext : ChildWorkflow is calling Activity: io.dapr.springboot.examples.wfp.child.ReverseActivity +2025-05-16T10:27:01.874+01:00 INFO 9855 --- [pool-3-thread-3] i.d.s.e.wfp.child.ReverseActivity : Starting Activity: io.dapr.springboot.examples.wfp.child.ReverseActivity +2025-05-16T10:27:01.874+01:00 INFO 9855 --- [pool-3-thread-3] i.d.s.e.wfp.child.ReverseActivity : Message Received from input: Hello Dapr Workflow! +2025-05-16T10:27:01.874+01:00 INFO 9855 --- [pool-3-thread-3] i.d.s.e.wfp.child.ReverseActivity : Sending message to output: !wolfkroW rpaD olleH +2025-05-16T10:27:01.882+01:00 INFO 9855 --- [pool-3-thread-1] io.dapr.workflows.WorkflowContext : ChildWorkflow finished with: !wolfkroW rpaD olleH +2025-05-16T10:27:01.892+01:00 INFO 9855 --- [pool-3-thread-2] io.dapr.workflows.WorkflowContext : childworkflow finished with: !wolfkroW rpaD olleH +``` + +### ContinueAsNew Workflows example + + +### External Event Workflow example + + +### Fan Out/In Workflow example + ## Testing workflow executions From e0e541f7a96d4342bb806c2d85276075e29fa217 Mon Sep 17 00:00:00 2001 From: salaboy Date: Fri, 16 May 2025 11:06:08 +0100 Subject: [PATCH 05/12] updating examples to work with markdown tests Signed-off-by: salaboy --- spring-boot-examples/workflows/README.md | 257 ++++++++++++++++-- spring-boot-examples/workflows/body.json | 5 + .../wfp/WorkflowPatternsRestController.java | 10 +- .../wfp/continueasnew/CleanUpActivity.java | 3 +- .../wfp/continueasnew/CleanUpLog.java | 17 +- .../continueasnew/ContinueAsNewWorkflow.java | 2 +- .../wfp/WorkflowPatternsAppTests.java | 2 +- 7 files changed, 255 insertions(+), 41 deletions(-) create mode 100644 spring-boot-examples/workflows/body.json diff --git a/spring-boot-examples/workflows/README.md b/spring-boot-examples/workflows/README.md index eb9b6a305b..b671155d75 100644 --- a/spring-boot-examples/workflows/README.md +++ b/spring-boot-examples/workflows/README.md @@ -88,18 +88,18 @@ TOKYO, LONDON, SEATTLE In the application output you should see the workflow activities being executed. ```bash -2025-05-16T09:59:22.176+01:00 INFO 8360 --- [pool-3-thread-1] io.dapr.workflows.WorkflowContext : Starting Workflow: io.dapr.springboot.examples.wfp.chain.ChainWorkflow -2025-05-16T09:59:22.194+01:00 INFO 8360 --- [nio-8080-exec-2] i.d.s.e.w.WorkflowPatternsRestController : Workflow instance 7625b4af-8c04-408a-93dc-bad753466e43 started -2025-05-16T09:59:22.195+01:00 INFO 8360 --- [pool-3-thread-2] i.d.s.e.wfp.chain.ToUpperCaseActivity : Starting Activity: io.dapr.springboot.examples.wfp.chain.ToUpperCaseActivity -2025-05-16T09:59:22.196+01:00 INFO 8360 --- [pool-3-thread-2] i.d.s.e.wfp.chain.ToUpperCaseActivity : Message Received from input: Tokyo -2025-05-16T09:59:22.197+01:00 INFO 8360 --- [pool-3-thread-2] i.d.s.e.wfp.chain.ToUpperCaseActivity : Sending message to output: TOKYO -2025-05-16T09:59:22.210+01:00 INFO 8360 --- [pool-3-thread-1] i.d.s.e.wfp.chain.ToUpperCaseActivity : Starting Activity: io.dapr.springboot.examples.wfp.chain.ToUpperCaseActivity -2025-05-16T09:59:22.210+01:00 INFO 8360 --- [pool-3-thread-1] i.d.s.e.wfp.chain.ToUpperCaseActivity : Message Received from input: London -2025-05-16T09:59:22.210+01:00 INFO 8360 --- [pool-3-thread-1] i.d.s.e.wfp.chain.ToUpperCaseActivity : Sending message to output: LONDON -2025-05-16T09:59:22.216+01:00 INFO 8360 --- [pool-3-thread-3] i.d.s.e.wfp.chain.ToUpperCaseActivity : Starting Activity: io.dapr.springboot.examples.wfp.chain.ToUpperCaseActivity -2025-05-16T09:59:22.216+01:00 INFO 8360 --- [pool-3-thread-3] i.d.s.e.wfp.chain.ToUpperCaseActivity : Message Received from input: Seattle -2025-05-16T09:59:22.216+01:00 INFO 8360 --- [pool-3-thread-3] i.d.s.e.wfp.chain.ToUpperCaseActivity : Sending message to output: SEATTLE -2025-05-16T09:59:22.219+01:00 INFO 8360 --- [pool-3-thread-1] io.dapr.workflows.WorkflowContext : Workflow finished with result: TOKYO, LONDON, SEATTLE +io.dapr.workflows.WorkflowContext : Starting Workflow: io.dapr.springboot.examples.wfp.chain.ChainWorkflow +i.d.s.e.w.WorkflowPatternsRestController : Workflow instance 7625b4af-8c04-408a-93dc-bad753466e43 started +i.d.s.e.wfp.chain.ToUpperCaseActivity : Starting Activity: io.dapr.springboot.examples.wfp.chain.ToUpperCaseActivity +i.d.s.e.wfp.chain.ToUpperCaseActivity : Message Received from input: Tokyo +i.d.s.e.wfp.chain.ToUpperCaseActivity : Sending message to output: TOKYO +i.d.s.e.wfp.chain.ToUpperCaseActivity : Starting Activity: io.dapr.springboot.examples.wfp.chain.ToUpperCaseActivity +i.d.s.e.wfp.chain.ToUpperCaseActivity : Message Received from input: London +i.d.s.e.wfp.chain.ToUpperCaseActivity : Sending message to output: LONDON +i.d.s.e.wfp.chain.ToUpperCaseActivity : Starting Activity: io.dapr.springboot.examples.wfp.chain.ToUpperCaseActivity +i.d.s.e.wfp.chain.ToUpperCaseActivity : Message Received from input: Seattle +i.d.s.e.wfp.chain.ToUpperCaseActivity : Sending message to output: SEATTLE +io.dapr.workflows.WorkflowContext : Workflow finished with result: TOKYO, LONDON, SEATTLE ``` ### Parent / Child Workflows example @@ -136,7 +136,6 @@ graph LR To start the parent workflow you can run: - + + +To start the workflow you can run: + +```sh +curl -X POST localhost:8080/wfp/continueasnew -H 'Content-Type: application/json' +``` + + + +As result from executing the request you should see: + +```bash +{"cleanUpTimes":5} +```` + +In the application output you should see the workflow activities being executed. + +```bash +io.dapr.workflows.WorkflowContext : Starting Workflow: io.dapr.springboot.examples.wfp.continueasnew.ContinueAsNewWorkflow +io.dapr.workflows.WorkflowContext : call CleanUpActivity to do the clean up +i.d.s.e.w.WorkflowPatternsRestController : Workflow instance b808e7d6-ab47-4eba-8188-dc9ff8780764 started +i.d.s.e.w.continueasnew.CleanUpActivity : Starting Activity: io.dapr.springboot.examples.wfp.continueasnew.CleanUpActivity +i.d.s.e.w.continueasnew.CleanUpActivity : start clean up work, it may take few seconds to finish... Time:10:48:45 +io.dapr.workflows.WorkflowContext : CleanUpActivity finished +io.dapr.workflows.WorkflowContext : wait 5 seconds for next clean up +io.dapr.workflows.WorkflowContext : Let's do more cleaning. +io.dapr.workflows.WorkflowContext : Starting Workflow: io.dapr.springboot.examples.wfp.continueasnew.ContinueAsNewWorkflow +io.dapr.workflows.WorkflowContext : call CleanUpActivity to do the clean up +i.d.s.e.w.continueasnew.CleanUpActivity : Starting Activity: io.dapr.springboot.examples.wfp.continueasnew.CleanUpActivity +i.d.s.e.w.continueasnew.CleanUpActivity : start clean up work, it may take few seconds to finish... Time:10:48:50 +io.dapr.workflows.WorkflowContext : CleanUpActivity finished +io.dapr.workflows.WorkflowContext : wait 5 seconds for next clean up +io.dapr.workflows.WorkflowContext : Let's do more cleaning. +io.dapr.workflows.WorkflowContext : Starting Workflow: io.dapr.springboot.examples.wfp.continueasnew.ContinueAsNewWorkflow +io.dapr.workflows.WorkflowContext : call CleanUpActivity to do the clean up +i.d.s.e.w.continueasnew.CleanUpActivity : Starting Activity: io.dapr.springboot.examples.wfp.continueasnew.CleanUpActivity +i.d.s.e.w.continueasnew.CleanUpActivity : start clean up work, it may take few seconds to finish... Time:10:48:55 +io.dapr.workflows.WorkflowContext : CleanUpActivity finished +io.dapr.workflows.WorkflowContext : wait 5 seconds for next clean up +io.dapr.workflows.WorkflowContext : Let's do more cleaning. +io.dapr.workflows.WorkflowContext : Starting Workflow: io.dapr.springboot.examples.wfp.continueasnew.ContinueAsNewWorkflow +io.dapr.workflows.WorkflowContext : call CleanUpActivity to do the clean up +i.d.s.e.w.continueasnew.CleanUpActivity : Starting Activity: io.dapr.springboot.examples.wfp.continueasnew.CleanUpActivity +i.d.s.e.w.continueasnew.CleanUpActivity : start clean up work, it may take few seconds to finish... Time:10:49:0 +io.dapr.workflows.WorkflowContext : CleanUpActivity finished +io.dapr.workflows.WorkflowContext : wait 5 seconds for next clean up +io.dapr.workflows.WorkflowContext : Let's do more cleaning. +io.dapr.workflows.WorkflowContext : Starting Workflow: io.dapr.springboot.examples.wfp.continueasnew.ContinueAsNewWorkflow +io.dapr.workflows.WorkflowContext : call CleanUpActivity to do the clean up +i.d.s.e.w.continueasnew.CleanUpActivity : Starting Activity: io.dapr.springboot.examples.wfp.continueasnew.CleanUpActivity +i.d.s.e.w.continueasnew.CleanUpActivity : start clean up work, it may take few seconds to finish... Time:10:49:5 +io.dapr.workflows.WorkflowContext : CleanUpActivity finished +io.dapr.workflows.WorkflowContext : wait 5 seconds for next clean up +io.dapr.workflows.WorkflowContext : We did enough cleaning +``` ### External Event Workflow example +In this example we start a workflow that as part of its execution waits for an external event to continue. To correlate +workflows and events we use the parameter `orderId` + +To start the workflow you can run: + + + + +To start the workflow you can run: + +```sh +curl -X POST "localhost:8080/wfp/externalevent?orderId=123" -H 'Content-Type: application/json' +``` + + + + +In the application output you should see the workflow activities being executed. + +```bash +io.dapr.workflows.WorkflowContext : Starting Workflow: io.dapr.springboot.examples.wfp.externalevent.ExternalEventWorkflow +io.dapr.workflows.WorkflowContext : Waiting for approval... +i.d.s.e.w.WorkflowPatternsRestController : Workflow instance 8a55cf6d-9059-49b1-8c83-fbe17567a02e started +``` + +You should see the Workflow ID that was created, in this example you don't need to remember this id, +as you can use the orderId to find the right instance. +When you are ready to approve the order you can send the following request: + + + + +To send the event you can run: + +```sh +curl -X POST "localhost:8080/wfp/externalevent-continue?orderId=123&decision=true" -H 'Content-Type: application/json' +``` + + + +```bash +{"approved":true} +``` + +In the application output you should see the workflow activities being executed. + +```bash +i.d.s.e.w.WorkflowPatternsRestController : Workflow instance e86bc464-6166-434d-8c91-d99040d6f54e continue +io.dapr.workflows.WorkflowContext : approval granted - do the approved action +i.d.s.e.w.externalevent.ApproveActivity : Starting Activity: io.dapr.springboot.examples.wfp.externalevent.ApproveActivity +i.d.s.e.w.externalevent.ApproveActivity : Running approval activity... +io.dapr.workflows.WorkflowContext : approval-activity finished +``` ### Fan Out/In Workflow example +In this example we start a workflow that takes an ArrayList of strings and calls one activity per item in the ArrayList. The activities +are executed and the workflow waits for all of them to complete to aggregate the results. + +```mermaid +graph LR + SW((Start + Workflow)) + subgraph for each word in the input + GWL[GetWordLength] + end + ALL[Wait until all tasks + are completed] + EW((End + Workflow)) + SW --> GWL + GWL --> ALL + ALL --> EW +``` + +To start the workflow you can run: + + + + +To start the workflow you can run: + +```sh +curl -X POST localhost:8080/wfp/fanoutin -H 'Content-Type: application/json' -d @body.json +``` + + + +As result from executing the request you should see: + +```bash +{"wordCount":60} +``` + +In the application output you should see the workflow activities being executed. + +```bash +io.dapr.workflows.WorkflowContext : Starting Workflow: io.dapr.springboot.examples.wfp.fanoutin.FanOutInWorkflow +i.d.s.e.w.WorkflowPatternsRestController : Workflow instance a771a7ba-f9fb-4399-aaee-a2fb0b102e5d started +i.d.s.e.wfp.fanoutin.CountWordsActivity : Starting Activity: io.dapr.springboot.examples.wfp.fanoutin.CountWordsActivity +i.d.s.e.wfp.fanoutin.CountWordsActivity : Starting Activity: io.dapr.springboot.examples.wfp.fanoutin.CountWordsActivity +i.d.s.e.wfp.fanoutin.CountWordsActivity : Starting Activity: io.dapr.springboot.examples.wfp.fanoutin.CountWordsActivity +i.d.s.e.wfp.fanoutin.CountWordsActivity : Activity returned: 2. +i.d.s.e.wfp.fanoutin.CountWordsActivity : Activity finished +i.d.s.e.wfp.fanoutin.CountWordsActivity : Activity returned: 11. +i.d.s.e.wfp.fanoutin.CountWordsActivity : Activity returned: 17. +i.d.s.e.wfp.fanoutin.CountWordsActivity : Activity finished +i.d.s.e.wfp.fanoutin.CountWordsActivity : Activity finished +i.d.s.e.wfp.fanoutin.CountWordsActivity : Starting Activity: io.dapr.springboot.examples.wfp.fanoutin.CountWordsActivity +i.d.s.e.wfp.fanoutin.CountWordsActivity : Activity returned: 21. +i.d.s.e.wfp.fanoutin.CountWordsActivity : Activity finished +i.d.s.e.wfp.fanoutin.CountWordsActivity : Starting Activity: io.dapr.springboot.examples.wfp.fanoutin.CountWordsActivity +i.d.s.e.wfp.fanoutin.CountWordsActivity : Activity returned: 9. +i.d.s.e.wfp.fanoutin.CountWordsActivity : Activity finished +io.dapr.workflows.WorkflowContext : Workflow finished with result: 60 +``` + ## Testing workflow executions diff --git a/spring-boot-examples/workflows/body.json b/spring-boot-examples/workflows/body.json new file mode 100644 index 0000000000..55471e3701 --- /dev/null +++ b/spring-boot-examples/workflows/body.json @@ -0,0 +1,5 @@ +["Hello, world!", + "The quick brown fox jumps over the lazy dog.", + "If a tree falls in the forest and there is no one there to hear it, does it make a sound?", + "The greatest glory in living lies not in never falling, but in rising every time we fall.", + "Always remember that you are absolutely unique. Just like everyone else."] \ No newline at end of file diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/WorkflowPatternsRestController.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/WorkflowPatternsRestController.java index da503d9304..cf1998907d 100644 --- a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/WorkflowPatternsRestController.java +++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/WorkflowPatternsRestController.java @@ -34,7 +34,9 @@ import org.springframework.web.bind.annotation.RestController; import java.time.Duration; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.TimeoutException; @RestController @@ -46,6 +48,8 @@ public class WorkflowPatternsRestController { @Autowired private DaprWorkflowClient daprWorkflowClient; + private Map ordersToApprove = new HashMap<>(); + @Bean public CleanUpLog cleanUpLog(){ return new CleanUpLog(); @@ -104,15 +108,17 @@ public Result faninout(@RequestBody List listOfStrings) throws TimeoutEx * @return confirmation that the workflow instance was created for the workflow pattern externalevent */ @PostMapping("wfp/externalevent") - public String externalevent() { + public String externalevent(@RequestParam("orderId") String orderId) { String instanceId = daprWorkflowClient.scheduleNewWorkflow(ExternalEventWorkflow.class); + ordersToApprove.put(orderId, instanceId); logger.info("Workflow instance " + instanceId + " started"); return instanceId; } @PostMapping("wfp/externalevent-continue") - public Decision externaleventContinue(@RequestParam("instanceId") String instanceId, @RequestParam("decision") Boolean decision) + public Decision externaleventContinue(@RequestParam("orderId") String orderId, @RequestParam("decision") Boolean decision) throws TimeoutException { + String instanceId = ordersToApprove.get(orderId); logger.info("Workflow instance " + instanceId + " continue"); daprWorkflowClient.raiseEvent(instanceId, "Approval", decision); WorkflowInstanceStatus workflowInstanceStatus = daprWorkflowClient diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/continueasnew/CleanUpActivity.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/continueasnew/CleanUpActivity.java index 2fcc8f01d4..cb9c7f7aa2 100644 --- a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/continueasnew/CleanUpActivity.java +++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/continueasnew/CleanUpActivity.java @@ -36,7 +36,6 @@ public Object run(WorkflowActivityContext ctx) { LocalDateTime now = LocalDateTime.now(); String cleanUpTimeString = now.getHour() + ":" + now.getMinute() + ":" + now.getSecond(); - cleanUpLog.getCleanUpTimes().add(cleanUpTimeString); logger.info("start clean up work, it may take few seconds to finish... Time:" + cleanUpTimeString); //Sleeping for 2 seconds to simulate long running operation @@ -46,6 +45,8 @@ public Object run(WorkflowActivityContext ctx) { throw new RuntimeException(e); } + cleanUpLog.increment(); + return "clean up finish."; } } diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/continueasnew/CleanUpLog.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/continueasnew/CleanUpLog.java index 34ceb0d8e5..9eb7c3fe9e 100644 --- a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/continueasnew/CleanUpLog.java +++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/continueasnew/CleanUpLog.java @@ -1,22 +1,15 @@ package io.dapr.springboot.examples.wfp.continueasnew; -import java.util.ArrayList; -import java.util.List; - public class CleanUpLog { - private List cleanUpTimes = new ArrayList<>(); + private Integer cleanUpTimes = 0; public CleanUpLog() { } - - public List getCleanUpTimes() { - return cleanUpTimes; + public void increment() { + this.cleanUpTimes += 1; } - @Override - public String toString() { - return "CleanUpLog{" + - "cleanUpTimes=" + cleanUpTimes + - '}'; + public Integer getCleanUpTimes() { + return cleanUpTimes; } } diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/continueasnew/ContinueAsNewWorkflow.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/continueasnew/ContinueAsNewWorkflow.java index 872d9ae268..ad05ec4c33 100644 --- a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/continueasnew/ContinueAsNewWorkflow.java +++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/continueasnew/ContinueAsNewWorkflow.java @@ -45,7 +45,7 @@ public WorkflowStub create() { ctx.getLogger().info("wait 5 seconds for next clean up"); ctx.createTimer(Duration.ofSeconds(3)).await(); - if(cleanUpLog.getCleanUpTimes().size() < 5) { + if(cleanUpLog.getCleanUpTimes() < 5) { // continue the workflow. ctx.getLogger().info("Let's do more cleaning."); ctx.continueAsNew(null); diff --git a/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/WorkflowPatternsAppTests.java b/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/WorkflowPatternsAppTests.java index 6643e78fe9..a6516725e3 100644 --- a/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/WorkflowPatternsAppTests.java +++ b/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/WorkflowPatternsAppTests.java @@ -134,7 +134,7 @@ void testContinueAsNew() { .then() .statusCode(200).extract().as(CleanUpLog.class); - assertEquals(5, cleanUpLog.getCleanUpTimes().size()); + assertEquals(5, cleanUpLog.getCleanUpTimes()); } } From fc1d7c9c7121d9ec0134b1f35b1af0173887c2dd Mon Sep 17 00:00:00 2001 From: salaboy Date: Fri, 16 May 2025 11:09:06 +0100 Subject: [PATCH 06/12] running mechanical markdown for workflow examples Signed-off-by: salaboy --- .github/workflows/validate.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 42a07c8785..efeb393910 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -216,4 +216,10 @@ jobs: run: | mm.py README.md env: - DOCKER_HOST: ${{steps.setup_docker.outputs.sock}} + DOCKER_HOST: ${{steps.setup_docker.outputs.sock}} + - name: Validate Spring Boot Workflow examples + working-directory: ./spring-boot-examples/workflows + run: | + mm.py README.md + env: + DOCKER_HOST: ${{steps.setup_docker.outputs.sock}} From 2469dfd03e41fcb53e54f7737adcc33ef6514086 Mon Sep 17 00:00:00 2001 From: Siri Varma Vegiraju Date: Thu, 15 May 2025 16:27:00 -0700 Subject: [PATCH 07/12] Fix the issue with retries not happening correctly for Activities and Workflows (#1343) * Add coverage for some properties (#1297) Signed-off-by: sirivarma * Make the DAPR version being used consistent across all tests (#1299) Signed-off-by: sirivarma * Separate Dapr constants from IT container constants (#1315) Signed-off-by: Artur Ciocanu Co-authored-by: Artur Ciocanu Signed-off-by: sirivarma * Use Java Bean for connection details and add more tests (#1317) * Use Java Bean for connection details and add more tests Signed-off-by: Artur Ciocanu * Simplify mock setup Signed-off-by: Artur Ciocanu * Adding even more tests for test coverage Signed-off-by: Artur Ciocanu --------- Signed-off-by: Artur Ciocanu Co-authored-by: Artur Ciocanu Co-authored-by: Cassie Coyle Signed-off-by: sirivarma * Update CONTRIBUTING.md Signed-off-by: Siri Varma Vegiraju Signed-off-by: sirivarma * Bump codecov/codecov-action from 5.4.0 to 5.4.2 (#1318) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.4.0 to 5.4.2. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v5.4.0...v5.4.2) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-version: 5.4.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Cassie Coyle Co-authored-by: Dapr Bot <56698301+dapr-bot@users.noreply.github.com> Signed-off-by: sirivarma * Fix URL building logic (#1320) * Fix URL building logic Signed-off-by: Artur Ciocanu * Add test for query params Signed-off-by: Artur Ciocanu * Fix the assertion in the test Signed-off-by: Artur Ciocanu * Adjust the tests Signed-off-by: Artur Ciocanu * Remove uneeded changes from IT test Signed-off-by: Artur Ciocanu * Revert some unintended changes Signed-off-by: Artur Ciocanu * Simplify the testing a little bit Signed-off-by: Artur Ciocanu * Adjust the test to use ServerRequest Signed-off-by: Artur Ciocanu * Test removing things from method invoke controller Signed-off-by: Artur Ciocanu * Add query param encoding test Signed-off-by: Artur Ciocanu * Revert some unintended changes Signed-off-by: Artur Ciocanu * Some tiny styles Signed-off-by: Artur Ciocanu --------- Signed-off-by: Artur Ciocanu Co-authored-by: Artur Ciocanu Signed-off-by: sirivarma * Generate updated javadocs for 1.14.1 Signed-off-by: Dapr Bot Signed-off-by: sirivarma * Add Conversation AI to Java SDK (#1235) * Conversation first commit Signed-off-by: Siri Varma Vegiraju Signed-off-by: sirivarma Signed-off-by: siri-varma * Add unit tests Signed-off-by: sirivarma Signed-off-by: siri-varma * change ai to conv Signed-off-by: sirivarma Signed-off-by: siri-varma * Move to single module Signed-off-by: sirivarma Signed-off-by: siri-varma * Remove module Signed-off-by: sirivarma Signed-off-by: siri-varma * Add Integration tests Signed-off-by: siri-varma * Update sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprConversationIT.java Co-authored-by: Cassie Coyle Signed-off-by: Siri Varma Vegiraju Signed-off-by: siri-varma * Fix things Signed-off-by: siri-varma * Address comments Signed-off-by: siri-varma * Import tag Signed-off-by: siri-varma * Address comments Signed-off-by: siri-varma * Make common config Signed-off-by: siri-varma * Address comments Signed-off-by: siri-varma * fix constant Signed-off-by: siri-varma * fix constant Signed-off-by: siri-varma * fix constant Signed-off-by: siri-varma * fix s Signed-off-by: siri-varma * Fix things Signed-off-by: siri-varma * Fix things Signed-off-by: siri-varma * Fix things Signed-off-by: siri-varma * Make common config Signed-off-by: siri-varma * Update README.md Signed-off-by: Siri Varma Vegiraju * Update README.md Signed-off-by: Siri Varma Vegiraju --------- Signed-off-by: Siri Varma Vegiraju Signed-off-by: sirivarma Signed-off-by: siri-varma Signed-off-by: Siri Varma Vegiraju Co-authored-by: Cassie Coyle Co-authored-by: Cassie Coyle Signed-off-by: sirivarma * Add docs for usage of Jobs SDK (#1323) * Add doc for jobs Signed-off-by: siri-varma * Add docs for Jobs Signed-off-by: siri-varma * Apply suggestions from code review Co-authored-by: Cassie Coyle Signed-off-by: Siri Varma Vegiraju --------- Signed-off-by: siri-varma Signed-off-by: Siri Varma Vegiraju Co-authored-by: artur-ciocanu Co-authored-by: Cassie Coyle Signed-off-by: sirivarma * Use dapr/durabletask-java (#1336) * microsoft durabletask-java -> dapr durabletask-java Signed-off-by: Cassandra Coyle * update another ref Signed-off-by: Cassandra Coyle * 1.5.2 release Signed-off-by: Cassandra Coyle * fix import order Signed-off-by: Cassandra Coyle * Sdk new changes Signed-off-by: siri-varma * Refine workflows Signed-off-by: siri-varma * add ; Signed-off-by: Cassandra Coyle * rm try Signed-off-by: Cassandra Coyle --------- Signed-off-by: Cassandra Coyle Signed-off-by: siri-varma Co-authored-by: siri-varma Signed-off-by: sirivarma * Update master version to 1.16.0-SNAPSHOT Signed-off-by: Dapr Bot Signed-off-by: sirivarma * Fix NPE Signed-off-by: siri-varma Signed-off-by: sirivarma * Fix NPE Signed-off-by: siri-varma Signed-off-by: sirivarma * Fix NPE Signed-off-by: siri-varma Signed-off-by: sirivarma * Fix NPE Signed-off-by: siri-varma Signed-off-by: sirivarma * Fix NPE Signed-off-by: siri-varma Signed-off-by: sirivarma * Fix NPE Signed-off-by: siri-varma Signed-off-by: sirivarma * Fix things Signed-off-by: siri-varma Signed-off-by: sirivarma * Renaming and exposing connection details (#1341) Signed-off-by: Artur Ciocanu Co-authored-by: Artur Ciocanu Signed-off-by: sirivarma * [Master] Fix Vulnerabilities (#1354) * update okio Signed-off-by: Cassandra Coyle * rm unused dep Signed-off-by: Cassandra Coyle --------- Signed-off-by: Cassandra Coyle Signed-off-by: sirivarma * Feat Add TLS & mTLS support for gRPC with root CA and insecure mode (#1361) * feat: Support for GRPC ssl Signed-off-by: Javier Aliaga * add tests Signed-off-by: Cassandra Coyle * fix CI Signed-off-by: Cassandra Coyle * add back else if Signed-off-by: Cassandra Coyle * channel cleanup Signed-off-by: Cassandra Coyle * add root ca support Signed-off-by: Cassandra Coyle * checkstyles Signed-off-by: Cassandra Coyle * add insecure Signed-off-by: Cassandra Coyle * fix checkstyles Signed-off-by: Cassandra Coyle * use InsecureTrustManagerFactory Signed-off-by: Cassandra Coyle * fix test Signed-off-by: Cassandra Coyle --------- Signed-off-by: Javier Aliaga Signed-off-by: Cassandra Coyle Co-authored-by: Javier Aliaga Signed-off-by: sirivarma * Address comments Signed-off-by: siri-varma Signed-off-by: sirivarma * Fix things Signed-off-by: siri-varma Signed-off-by: sirivarma * Fix things Signed-off-by: siri-varma Signed-off-by: sirivarma --------- Signed-off-by: sirivarma Signed-off-by: Artur Ciocanu Signed-off-by: Siri Varma Vegiraju Signed-off-by: dependabot[bot] Signed-off-by: Dapr Bot Signed-off-by: Siri Varma Vegiraju Signed-off-by: siri-varma Signed-off-by: Cassandra Coyle Signed-off-by: Javier Aliaga Co-authored-by: Matheus Cruz <56329339+mcruzdev@users.noreply.github.com> Co-authored-by: artur-ciocanu Co-authored-by: Artur Ciocanu Co-authored-by: Cassie Coyle Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Dapr Bot <56698301+dapr-bot@users.noreply.github.com> Co-authored-by: Dapr Bot Co-authored-by: Cassie Coyle Co-authored-by: Javier Aliaga Signed-off-by: salaboy --- .../childworkflow/DemoChildWorkflow.java | 13 ++++- .../DemoChildWorkflowWorker.java | 1 + sdk-workflows/pom.xml | 2 +- .../workflows/WorkflowTaskRetryPolicy.java | 5 +- .../runtime/DefaultWorkflowContext.java | 4 +- .../workflows/DefaultWorkflowContextTest.java | 49 +++++++++++++++++++ 6 files changed, 69 insertions(+), 5 deletions(-) diff --git a/examples/src/main/java/io/dapr/examples/workflows/childworkflow/DemoChildWorkflow.java b/examples/src/main/java/io/dapr/examples/workflows/childworkflow/DemoChildWorkflow.java index c9a9af4557..4c1ac8ddb4 100644 --- a/examples/src/main/java/io/dapr/examples/workflows/childworkflow/DemoChildWorkflow.java +++ b/examples/src/main/java/io/dapr/examples/workflows/childworkflow/DemoChildWorkflow.java @@ -15,6 +15,10 @@ import io.dapr.workflows.Workflow; import io.dapr.workflows.WorkflowStub; +import io.dapr.workflows.WorkflowTaskOptions; +import io.dapr.workflows.WorkflowTaskRetryPolicy; + +import java.time.Duration; public class DemoChildWorkflow implements Workflow { @Override @@ -22,11 +26,18 @@ public WorkflowStub create() { return ctx -> { ctx.getLogger().info("Starting ChildWorkflow: " + ctx.getName()); + WorkflowTaskRetryPolicy policy = WorkflowTaskRetryPolicy.newBuilder() + .setFirstRetryInterval(Duration.ofSeconds(1)) + .setMaxNumberOfAttempts(10) + .build(); + + WorkflowTaskOptions options = new WorkflowTaskOptions(policy); + var childWorkflowInput = ctx.getInput(String.class); ctx.getLogger().info("ChildWorkflow received input: " + childWorkflowInput); ctx.getLogger().info("ChildWorkflow is calling Activity: " + ReverseActivity.class.getName()); - String result = ctx.callActivity(ReverseActivity.class.getName(), childWorkflowInput, String.class).await(); + String result = ctx.callActivity(ReverseActivity.class.getName(), childWorkflowInput, options, String.class).await(); ctx.getLogger().info("ChildWorkflow finished with: " + result); ctx.complete(result); diff --git a/examples/src/main/java/io/dapr/examples/workflows/childworkflow/DemoChildWorkflowWorker.java b/examples/src/main/java/io/dapr/examples/workflows/childworkflow/DemoChildWorkflowWorker.java index 0e692551e5..dce40e97e3 100644 --- a/examples/src/main/java/io/dapr/examples/workflows/childworkflow/DemoChildWorkflowWorker.java +++ b/examples/src/main/java/io/dapr/examples/workflows/childworkflow/DemoChildWorkflowWorker.java @@ -32,6 +32,7 @@ public static void main(String[] args) throws Exception { // Build and then start the workflow runtime pulling and executing tasks WorkflowRuntime runtime = builder.build(); + runtime.start(); System.out.println("Start workflow runtime"); } } diff --git a/sdk-workflows/pom.xml b/sdk-workflows/pom.xml index 0a5289daf6..e763e7630d 100644 --- a/sdk-workflows/pom.xml +++ b/sdk-workflows/pom.xml @@ -47,7 +47,7 @@ io.dapr durabletask-client - 1.5.2 + 1.5.3 ```sh -cd workflows/ ../../mvnw spring-boot:test-run ``` @@ -193,8 +192,8 @@ output_match_mode: substring expected_stdout_lines: - '{"cleanUpTimes":5}' background: true -sleep: 1 -timeout_seconds: 2 +sleep: 10 +timeout_seconds: 15 --> @@ -298,8 +297,8 @@ output_match_mode: substring expected_stdout_lines: - '{"approved":true}' background: true -sleep: 1 -timeout_seconds: 2 +sleep: 5 +timeout_seconds: 10 --> From 63b2d6ce750c062eee46a8df6d7b8e2c29172116 Mon Sep 17 00:00:00 2001 From: salaboy Date: Fri, 16 May 2025 15:03:36 +0100 Subject: [PATCH 11/12] fixing app name Signed-off-by: salaboy --- .../workflows/src/main/resources/application.properties | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/spring-boot-examples/workflows/src/main/resources/application.properties b/spring-boot-examples/workflows/src/main/resources/application.properties index 1498965c7d..7fd4ccd5b9 100644 --- a/spring-boot-examples/workflows/src/main/resources/application.properties +++ b/spring-boot-examples/workflows/src/main/resources/application.properties @@ -1,4 +1 @@ -spring.application.name=producer-app -dapr.pubsub.name=pubsub -dapr.statestore.name=kvstore -dapr.statestore.binding=kvbinding +spring.application.name=workflow-patterns-app From aa7e98009ceb9098e4f292214f8b70da201ee4d9 Mon Sep 17 00:00:00 2001 From: salaboy Date: Sat, 17 May 2025 11:00:36 +0100 Subject: [PATCH 12/12] adding app name and removing log lines Signed-off-by: salaboy --- .../dapr/springboot/examples/wfp/DaprTestContainersConfig.java | 2 -- .../workflows/src/test/resources/application.properties | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/DaprTestContainersConfig.java b/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/DaprTestContainersConfig.java index a182af1c44..a0e3a087ce 100644 --- a/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/DaprTestContainersConfig.java +++ b/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/DaprTestContainersConfig.java @@ -33,8 +33,6 @@ public DaprContainer daprContainer() { return new DaprContainer(DAPR_RUNTIME_IMAGE_TAG) .withAppName("workflow-patterns-app") .withComponent(new Component("kvstore", "state.in-memory", "v1", Collections.singletonMap("actorStateStore", String.valueOf(true)))) -// .withDaprLogLevel(DaprLogLevel.DEBUG) -// .withLogConsumer(outputFrame -> System.out.println(outputFrame.getUtf8String())) .withAppPort(8080) .withAppHealthCheckPath("/actuator/health") .withAppChannelAddress("host.testcontainers.internal"); diff --git a/spring-boot-examples/workflows/src/test/resources/application.properties b/spring-boot-examples/workflows/src/test/resources/application.properties index e69de29bb2..20d5fc037f 100644 --- a/spring-boot-examples/workflows/src/test/resources/application.properties +++ b/spring-boot-examples/workflows/src/test/resources/application.properties @@ -0,0 +1 @@ +spring.application.name=workflow-patterns-app \ No newline at end of file