diff --git a/conductor-clients/README.md b/conductor-clients/README.md
new file mode 100644
index 000000000..40782e081
--- /dev/null
+++ b/conductor-clients/README.md
@@ -0,0 +1 @@
+# Conductor Clients
diff --git a/conductor-clients/java/conductor-java-sdk/LICENSE b/conductor-clients/java/conductor-java-sdk/LICENSE
new file mode 100644
index 000000000..6a1d025d8
--- /dev/null
+++ b/conductor-clients/java/conductor-java-sdk/LICENSE
@@ -0,0 +1,201 @@
+Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright {yyyy} Netflix, Inc.
+
+ 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.
\ No newline at end of file
diff --git a/conductor-clients/java/conductor-java-sdk/README.md b/conductor-clients/java/conductor-java-sdk/README.md
new file mode 100644
index 000000000..d8bc0d304
--- /dev/null
+++ b/conductor-clients/java/conductor-java-sdk/README.md
@@ -0,0 +1,89 @@
+# Conductor Java Client/SDK V3
+
+## Overview
+
+**This project is currently in incubating status**.
+
+It is under active development and subject to changes as it evolves. While the core features are functional, the project is not yet considered stable, and breaking changes may occur as we refine the architecture and add new functionality.
+
+These changes are largely driven by **"dependency optimization"** and a redesign of the client to introduces filters, events and listeners to allow extensibility through callbacks or Event-Driven Architecture, IoC.
+
+This client has a reduced dependency set. The aim is to minimize Classpath pollution and prevent potential conflicts.
+
+Take Netflix Eureka as an example, Spring Cloud users have reported version conflicts. Some of them weren't even using Eureka. So, we've decided to remove the direct dependency.
+
+In the client it's used by the `TaskPollExecutor` before polling to make the following check:
+
+```java
+if (eurekaClient != null
+ && !eurekaClient.getInstanceRemoteStatus().equals(InstanceStatus.UP)
+ && !discoveryOverride) {
+ LOGGER.debug("Instance is NOT UP in discovery - will not poll");
+ return;
+}
+```
+
+You will be able to achieve the same with a `PollFilter`. It could look something like this:
+
+```java
+ var runnerConfigurer = new TaskRunnerConfigurer
+ .Builder(taskClient, List.of(new ApprovalWorker()))
+ .withThreadCount(10)
+ .withPollFilter((String taskType, String domain) -> {
+ return eurekaClient.getInstanceRemoteStatus().equals(InstanceStatus.UP);
+ })
+ .withListener(PollCompleted.class, (e) -> {
+ log.info("Poll Completed {}", e);
+ var timer = prometheusRegistry.timer("poll_success", "type", e.getTaskType());
+ timer.record(e.getDuration());
+ })
+ .withListener(TaskExecutionFailure.class, (e) -> {
+ log.error("Task Execution Failure {}", e);
+ var counter = prometheusRegistry.counter("execute_failure", "type", e.getTaskType(), "id", e.getTaskId());
+ counter.increment();
+ })
+ .build();
+runnerConfigurer.init();
+```
+
+The telemetry part was also removed but you can achieve the same with Events and Listeners as shown in the example.
+
+### Breaking Changes
+
+While we aim to minimize breaking changes, there are a few areas where such changes are necessary.
+
+Below are two specific examples of where changes may affect your existing code:
+
+#### (1) Jersey Config
+
+The `WorkflowClient` and other clients will retain the same methods, but constructors with dependencies on Jersey are being removed. For example:
+
+```java
+public WorkflowClient(ClientConfig config, ClientHandler handler) {
+ this(config, new DefaultConductorClientConfiguration(), handler);
+}
+```
+
+#### (2) Eureka Client
+
+In the Worker API we've removed the Eureka Client configuration option (from `TaskRunnerConfigurer`).
+
+```java
+* @param eurekaClient Eureka client - used to identify if the server is in discovery or
+ * not. When the server goes out of discovery, the polling is terminated. If passed
+ * null, discovery check is not done.
+ * @return Builder instance
+ */
+public Builder withEurekaClient(EurekaClient eurekaClient) {
+ this.eurekaClient = eurekaClient;
+ return this;
+}
+```
+
+## TODO
+
+- Stabilize the codebase
+- Complete documentation
+- Gather community feedback
+- Achieve production readiness
+
diff --git a/conductor-clients/java/conductor-java-sdk/build.gradle b/conductor-clients/java/conductor-java-sdk/build.gradle
new file mode 100644
index 000000000..5c402e1a7
--- /dev/null
+++ b/conductor-clients/java/conductor-java-sdk/build.gradle
@@ -0,0 +1,55 @@
+plugins {
+ id 'com.diffplug.spotless' version '6.+'
+}
+
+group = 'io.orkes.conductor'
+
+apply from: "$rootDir/versions.gradle"
+
+subprojects {
+ apply plugin: 'java'
+ apply plugin: 'com.diffplug.spotless'
+ group = 'io.orkes.conductor'
+
+ spotless {
+ java {
+ removeUnusedImports()
+ importOrder('java', 'javax', 'org', 'com.netflix', 'io.orkes', '', '\\#com.netflix', '\\#')
+ licenseHeaderFile("$rootDir/licenseheader.txt")
+ }
+ }
+
+ compileJava {
+ sourceCompatibility = 11
+ targetCompatibility = 11
+ }
+
+ dependencies {
+ // ### candidates to be shadowed ###
+ implementation "com.google.guava:guava:${versions.guava}"
+ // #################################
+
+ implementation "com.fasterxml.jackson.core:jackson-databind:${versions.jackson}"
+ implementation "com.fasterxml.jackson.core:jackson-core:${versions.jackson}"
+ implementation "com.fasterxml.jackson.core:jackson-annotations:${versions.jackson}"
+ implementation "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:${versions.jackson}"
+
+ implementation "org.slf4j:slf4j-api:${versions.slf4j}"
+ implementation "org.apache.commons:commons-lang3:${versions.commonsLang}"
+ annotationProcessor "org.projectlombok:lombok:${versions.lombok}"
+ compileOnly "org.projectlombok:lombok:${versions.lombok}"
+ }
+
+ repositories {
+ mavenCentral()
+ mavenLocal()
+ maven {
+ url "https://s01.oss.sonatype.org/content/repositories/releases/"
+ }
+ }
+
+ tasks.withType(Javadoc) {
+ options.addStringOption('Xdoclint:none', '-quiet')
+ }
+
+}
diff --git a/conductor-clients/java/conductor-java-sdk/conductor-client-metrics/build.gradle b/conductor-clients/java/conductor-java-sdk/conductor-client-metrics/build.gradle
new file mode 100644
index 000000000..62c67e7e8
--- /dev/null
+++ b/conductor-clients/java/conductor-java-sdk/conductor-client-metrics/build.gradle
@@ -0,0 +1,90 @@
+plugins {
+ id 'java-library'
+ id 'idea'
+ id 'maven-publish'
+ id 'signing'
+}
+
+dependencies {
+ implementation 'io.micrometer:micrometer-registry-prometheus:1.10.5'
+ implementation project(":conductor-client")
+
+ testImplementation 'org.mockito:mockito-core:5.4.0'
+ testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
+ testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
+}
+
+java {
+ withSourcesJar()
+ withJavadocJar()
+}
+
+publishing {
+ publications {
+ mavenJava(MavenPublication) {
+ from components.java
+ pom {
+ name = 'Orkes Conductor Metrics module'
+ description = 'OSS & Orkes Conductor Metrics'
+ url = 'https://github.com/conductor-oss/conductor.git'
+ scm {
+ connection = 'scm:git:git://github.com/conductor-oss/conductor.git'
+ developerConnection = 'scm:git:ssh://github.com/conductor-oss/conductor.git'
+ url = 'https://github.com/conductor-oss/conductor.git'
+ }
+ licenses {
+ license {
+ name = 'The Apache License, Version 2.0'
+ url = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
+ }
+ }
+ developers {
+ developer {
+ organization = 'Orkes'
+ organizationUrl = 'https://orkes.io'
+ name = 'Orkes Development Team'
+ email = 'developers@orkes.io'
+ }
+ }
+ }
+ }
+ }
+
+ repositories {
+ maven {
+ if (project.hasProperty("mavenCentral")) {
+ println "Publishing to Sonatype Repository"
+ url = "https://s01.oss.sonatype.org/${project.version.endsWith('-SNAPSHOT') ? "content/repositories/snapshots/" : "service/local/staging/deploy/maven2/"}"
+ credentials {
+ username project.properties.username
+ password project.properties.password
+ }
+ } else {
+ url = "s3://orkes-artifacts-repo/${project.version.endsWith('-SNAPSHOT') ? "snapshots" : "releases"}"
+ authentication {
+ awsIm(AwsImAuthentication)
+ }
+ }
+ }
+ }
+}
+
+signing {
+ def signingKeyId = findProperty('signingKeyId')
+ if (signingKeyId) {
+ println 'Signing the artifact with keys'
+ signing {
+ def signingKey = findProperty('signingKey')
+ def signingPassword = findProperty('signingPassword')
+ if (signingKeyId && signingKey && signingPassword) {
+ useInMemoryPgpKeys(signingKeyId, signingKey, signingPassword)
+ }
+
+ sign publishing.publications
+ }
+ }
+}
+
+test {
+ useJUnitPlatform()
+}
\ No newline at end of file
diff --git a/conductor-clients/java/conductor-java-sdk/conductor-client-metrics/src/main/java/com/netflix/conductor/client/metrics/prometheus/PrometheusMetricsCollector.java b/conductor-clients/java/conductor-java-sdk/conductor-client-metrics/src/main/java/com/netflix/conductor/client/metrics/prometheus/PrometheusMetricsCollector.java
new file mode 100644
index 000000000..307da9109
--- /dev/null
+++ b/conductor-clients/java/conductor-java-sdk/conductor-client-metrics/src/main/java/com/netflix/conductor/client/metrics/prometheus/PrometheusMetricsCollector.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2024 Conductor 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 com.netflix.conductor.client.metrics.prometheus;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+
+import com.netflix.conductor.client.automator.events.PollCompleted;
+import com.netflix.conductor.client.automator.events.PollFailure;
+import com.netflix.conductor.client.automator.events.PollStarted;
+import com.netflix.conductor.client.automator.events.TaskExecutionCompleted;
+import com.netflix.conductor.client.automator.events.TaskExecutionFailure;
+import com.netflix.conductor.client.automator.events.TaskExecutionStarted;
+import com.netflix.conductor.client.metrics.MetricsCollector;
+
+import com.sun.net.httpserver.HttpServer;
+import io.micrometer.prometheus.PrometheusConfig;
+import io.micrometer.prometheus.PrometheusMeterRegistry;
+
+public class PrometheusMetricsCollector implements MetricsCollector {
+
+ private static final PrometheusMeterRegistry prometheusRegistry = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT);
+
+ public void startServer() throws IOException {
+ startServer(9991, "/metrics");
+ }
+
+ public void startServer(int port, String endpoint) throws IOException {
+ var server = HttpServer.create(new InetSocketAddress(port), 0);
+ server.createContext(endpoint, (exchange -> {
+ var body = prometheusRegistry.scrape();
+ exchange.getResponseHeaders().set("Content-Type", "text/plain");
+ exchange.sendResponseHeaders(200, body.getBytes().length);
+ try (var os = exchange.getResponseBody()) {
+ os.write(body.getBytes());
+ }
+ }));
+ server.start();
+ }
+
+ @Override
+ public void consume(PollFailure e) {
+ var timer = prometheusRegistry.timer("poll_failure", "type", e.getTaskType());
+ timer.record(e.getDuration());
+ }
+
+ @Override
+ public void consume(PollCompleted e) {
+ var timer = prometheusRegistry.timer("poll_success", "type", e.getTaskType());
+ timer.record(e.getDuration());
+ }
+
+ @Override
+ public void consume(PollStarted e) {
+ var counter = prometheusRegistry.counter("poll_started", "type", e.getTaskType());
+ counter.increment();
+ }
+
+ @Override
+ public void consume(TaskExecutionStarted e) {
+ var counter = prometheusRegistry.counter("task_execution_started", "type", e.getTaskType());
+ counter.increment();
+ }
+
+ @Override
+ public void consume(TaskExecutionCompleted e) {
+ var timer = prometheusRegistry.timer("task_execution_completed", "type", e.getTaskType());
+ timer.record(e.getDuration());
+ }
+
+ @Override
+ public void consume(TaskExecutionFailure e) {
+ var timer = prometheusRegistry.timer("task_execution_failure", "type", e.getTaskType());
+ timer.record(e.getDuration());
+ }
+}
diff --git a/conductor-clients/java/conductor-java-sdk/conductor-client-spring/build.gradle b/conductor-clients/java/conductor-java-sdk/conductor-client-spring/build.gradle
new file mode 100644
index 000000000..3966fa469
--- /dev/null
+++ b/conductor-clients/java/conductor-java-sdk/conductor-client-spring/build.gradle
@@ -0,0 +1,76 @@
+plugins {
+ id 'java-library'
+ id 'idea'
+ id 'maven-publish'
+ id 'signing'
+}
+
+compileJava {
+ sourceCompatibility = 17
+ targetCompatibility = 17
+}
+
+repositories {
+ mavenCentral()
+}
+
+dependencies {
+ api project(":conductor-client")
+ api project(":sdk")
+
+ implementation 'org.springframework.boot:spring-boot-starter:3.3.0'
+}
+
+publishing {
+ publications {
+ mavenJava(MavenPublication) {
+ from components.java
+ pom {
+ name = 'Conductor OSS Client Spring'
+ description = 'Spring autoconfig for Conductor Client and SDK'
+ url = 'https://github.com/conductor-oss/conductor.git'
+ scm {
+ connection = 'scm:git:git://github.com/conductor-oss/conductor.git'
+ developerConnection = 'scm:git:ssh://github.com/conductor-oss/conductor.git'
+ url = 'https://github.com/conductor-oss/conductor.git'
+ }
+ licenses {
+ license {
+ name = 'The Apache License, Version 2.0'
+ url = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
+ }
+ }
+ developers {
+ developer {
+ organization = 'Orkes'
+ organizationUrl = 'https://orkes.io'
+ name = 'Orkes Development Team'
+ email = 'developers@orkes.io'
+ }
+ }
+ }
+ }
+ }
+
+ repositories {
+ maven {
+ if (project.hasProperty("mavenCentral")) {
+ println "Publishing to Sonatype Repository"
+ url = "https://s01.oss.sonatype.org/${project.version.endsWith('-SNAPSHOT') ? "content/repositories/snapshots/" : "service/local/staging/deploy/maven2/"}"
+ credentials {
+ username project.properties.username
+ password project.properties.password
+ }
+ } else {
+ url = "s3://orkes-artifacts-repo/${project.version.endsWith('-SNAPSHOT') ? "snapshots" : "releases"}"
+ authentication {
+ awsIm(AwsImAuthentication)
+ }
+ }
+ }
+ }
+}
+
+test {
+ useJUnitPlatform()
+}
\ No newline at end of file
diff --git a/conductor-clients/java/conductor-java-sdk/conductor-client-spring/src/main/java/com/netflix/conductor/client/spring/ClientProperties.java b/conductor-clients/java/conductor-java-sdk/conductor-client-spring/src/main/java/com/netflix/conductor/client/spring/ClientProperties.java
new file mode 100644
index 000000000..f459c1cd7
--- /dev/null
+++ b/conductor-clients/java/conductor-java-sdk/conductor-client-spring/src/main/java/com/netflix/conductor/client/spring/ClientProperties.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2020 Conductor 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 com.netflix.conductor.client.spring;
+
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+@ConfigurationProperties("conductor.client")
+public class ClientProperties {
+
+ private String rootUri;
+
+ private String workerNamePrefix = "workflow-worker-%d";
+
+ private int threadCount = 1;
+
+ private Duration sleepWhenRetryDuration = Duration.ofMillis(500);
+
+ private int updateRetryCount = 3;
+
+ private Map taskToDomain = new HashMap<>();
+
+ private Map taskThreadCount = new HashMap<>();
+
+ private int shutdownGracePeriodSeconds = 10;
+
+ private int taskPollTimeout = 100;
+
+ public String getRootUri() {
+ return rootUri;
+ }
+
+ public void setRootUri(String rootUri) {
+ this.rootUri = rootUri;
+ }
+
+ public String getWorkerNamePrefix() {
+ return workerNamePrefix;
+ }
+
+ public void setWorkerNamePrefix(String workerNamePrefix) {
+ this.workerNamePrefix = workerNamePrefix;
+ }
+
+ public int getThreadCount() {
+ return threadCount;
+ }
+
+ public void setThreadCount(int threadCount) {
+ this.threadCount = threadCount;
+ }
+
+ public Duration getSleepWhenRetryDuration() {
+ return sleepWhenRetryDuration;
+ }
+
+ public void setSleepWhenRetryDuration(Duration sleepWhenRetryDuration) {
+ this.sleepWhenRetryDuration = sleepWhenRetryDuration;
+ }
+
+ public int getUpdateRetryCount() {
+ return updateRetryCount;
+ }
+
+ public void setUpdateRetryCount(int updateRetryCount) {
+ this.updateRetryCount = updateRetryCount;
+ }
+
+ public Map getTaskToDomain() {
+ return taskToDomain;
+ }
+
+ public void setTaskToDomain(Map taskToDomain) {
+ this.taskToDomain = taskToDomain;
+ }
+
+ public int getShutdownGracePeriodSeconds() {
+ return shutdownGracePeriodSeconds;
+ }
+
+ public void setShutdownGracePeriodSeconds(int shutdownGracePeriodSeconds) {
+ this.shutdownGracePeriodSeconds = shutdownGracePeriodSeconds;
+ }
+
+ public Map getTaskThreadCount() {
+ return taskThreadCount;
+ }
+
+ public void setTaskThreadCount(Map taskThreadCount) {
+ this.taskThreadCount = taskThreadCount;
+ }
+
+ public int getTaskPollTimeout() {
+ return taskPollTimeout;
+ }
+
+ public void setTaskPollTimeout(int taskPollTimeout) {
+ this.taskPollTimeout = taskPollTimeout;
+ }
+}
diff --git a/conductor-clients/java/conductor-java-sdk/conductor-client-spring/src/main/java/com/netflix/conductor/client/spring/ConductorClientAutoConfiguration.java b/conductor-clients/java/conductor-java-sdk/conductor-client-spring/src/main/java/com/netflix/conductor/client/spring/ConductorClientAutoConfiguration.java
new file mode 100644
index 000000000..194ad6cf6
--- /dev/null
+++ b/conductor-clients/java/conductor-java-sdk/conductor-client-spring/src/main/java/com/netflix/conductor/client/spring/ConductorClientAutoConfiguration.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2020 Conductor 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 com.netflix.conductor.client.spring;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.env.Environment;
+
+import com.netflix.conductor.client.automator.TaskRunnerConfigurer;
+import com.netflix.conductor.client.http.ConductorClient;
+import com.netflix.conductor.client.http.TaskClient;
+import com.netflix.conductor.client.http.WorkflowClient;
+import com.netflix.conductor.client.worker.Worker;
+import com.netflix.conductor.sdk.workflow.executor.WorkflowExecutor;
+import com.netflix.conductor.sdk.workflow.executor.task.AnnotatedWorkerExecutor;
+
+import lombok.extern.slf4j.Slf4j;
+
+@Configuration(proxyBeanMethods = false)
+@EnableConfigurationProperties(ClientProperties.class)
+@Slf4j
+public class ConductorClientAutoConfiguration {
+
+ @ConditionalOnMissingBean
+ @Bean
+ public ConductorClient conductorClient(ClientProperties clientProperties) {
+ // TODO allow configuration of other properties via application.properties
+ return ConductorClient.builder()
+ .basePath(clientProperties.getRootUri())
+ .build();
+ }
+
+ @ConditionalOnMissingBean
+ @Bean
+ public TaskClient taskClient(ConductorClient client) {
+ return new TaskClient(client);
+ }
+
+ @ConditionalOnMissingBean
+ @Bean
+ public AnnotatedWorkerExecutor annotatedWorkerExecutor(TaskClient taskClient) {
+ return new AnnotatedWorkerExecutor(taskClient);
+ }
+
+ @ConditionalOnMissingBean
+ @Bean(initMethod = "init", destroyMethod = "shutdown")
+ public TaskRunnerConfigurer taskRunnerConfigurer(Environment env,
+ TaskClient taskClient,
+ ClientProperties clientProperties,
+ List workers) {
+ Map taskThreadCount = new HashMap<>();
+ for (Worker worker : workers) {
+ String key = "conductor.worker." + worker.getTaskDefName() + ".threadCount";
+ int threadCount = env.getProperty(key, Integer.class, 10);
+ log.info("Using {} threads for {} worker", threadCount, worker.getTaskDefName());
+ taskThreadCount.put(worker.getTaskDefName(), threadCount);
+ }
+
+ if (clientProperties.getTaskThreadCount() != null) {
+ clientProperties.getTaskThreadCount().putAll(taskThreadCount);
+ } else {
+ clientProperties.setTaskThreadCount(taskThreadCount);
+ }
+
+ return new TaskRunnerConfigurer.Builder(taskClient, workers)
+ .withTaskThreadCount(clientProperties.getTaskThreadCount())
+ .withThreadCount(clientProperties.getThreadCount())
+ .withSleepWhenRetry((int) clientProperties.getSleepWhenRetryDuration().toMillis())
+ .withUpdateRetryCount(clientProperties.getUpdateRetryCount())
+ .withTaskToDomain(clientProperties.getTaskToDomain())
+ .withShutdownGracePeriodSeconds(clientProperties.getShutdownGracePeriodSeconds())
+ .withTaskPollTimeout(clientProperties.getTaskPollTimeout())
+ .build();
+ }
+
+ @Bean
+ public WorkflowExecutor workflowExecutor(ConductorClient client, AnnotatedWorkerExecutor annotatedWorkerExecutor) {
+ return new WorkflowExecutor(client, annotatedWorkerExecutor);
+ }
+
+ @ConditionalOnMissingBean
+ @Bean
+ public WorkflowClient workflowClient(ConductorClient client) {
+ return new WorkflowClient(client);
+ }
+}
diff --git a/conductor-clients/java/conductor-java-sdk/conductor-client-spring/src/main/java/com/netflix/conductor/client/spring/ConductorWorkerAutoConfiguration.java b/conductor-clients/java/conductor-java-sdk/conductor-client-spring/src/main/java/com/netflix/conductor/client/spring/ConductorWorkerAutoConfiguration.java
new file mode 100644
index 000000000..cdc212c5e
--- /dev/null
+++ b/conductor-clients/java/conductor-java-sdk/conductor-client-spring/src/main/java/com/netflix/conductor/client/spring/ConductorWorkerAutoConfiguration.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2023 Conductor 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 com.netflix.conductor.client.spring;
+
+import java.util.Map;
+
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationListener;
+import org.springframework.context.event.ContextRefreshedEvent;
+import org.springframework.core.env.Environment;
+import org.springframework.stereotype.Component;
+
+import com.netflix.conductor.client.http.TaskClient;
+import com.netflix.conductor.sdk.workflow.executor.task.AnnotatedWorkerExecutor;
+import com.netflix.conductor.sdk.workflow.executor.task.WorkerConfiguration;
+
+@Component
+public class ConductorWorkerAutoConfiguration implements ApplicationListener {
+
+ private final TaskClient taskClient;
+
+ public ConductorWorkerAutoConfiguration(TaskClient taskClient) {
+ this.taskClient = taskClient;
+ }
+
+ @Override
+ public void onApplicationEvent(ContextRefreshedEvent refreshedEvent) {
+ ApplicationContext applicationContext = refreshedEvent.getApplicationContext();
+ Environment environment = applicationContext.getEnvironment();
+ WorkerConfiguration configuration = new SpringWorkerConfiguration(environment);
+ AnnotatedWorkerExecutor annotatedWorkerExecutor = new AnnotatedWorkerExecutor(taskClient, configuration);
+
+ Map beans = applicationContext.getBeansWithAnnotation(Component.class);
+ beans.values().forEach(annotatedWorkerExecutor::addBean);
+ annotatedWorkerExecutor.startPolling();
+ }
+}
diff --git a/conductor-clients/java/conductor-java-sdk/conductor-client-spring/src/main/java/com/netflix/conductor/client/spring/SpringWorkerConfiguration.java b/conductor-clients/java/conductor-java-sdk/conductor-client-spring/src/main/java/com/netflix/conductor/client/spring/SpringWorkerConfiguration.java
new file mode 100644
index 000000000..8dce540e0
--- /dev/null
+++ b/conductor-clients/java/conductor-java-sdk/conductor-client-spring/src/main/java/com/netflix/conductor/client/spring/SpringWorkerConfiguration.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2023 Conductor 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 com.netflix.conductor.client.spring;
+
+import org.springframework.core.env.Environment;
+
+import com.netflix.conductor.sdk.workflow.executor.task.WorkerConfiguration;
+
+class SpringWorkerConfiguration extends WorkerConfiguration {
+
+ private final Environment environment;
+
+ public SpringWorkerConfiguration(Environment environment) {
+ this.environment = environment;
+ }
+
+ @Override
+ public int getPollingInterval(String taskName) {
+ return getProperty(taskName, "pollingInterval", Integer.class, 0);
+ }
+
+ @Override
+ public int getThreadCount(String taskName) {
+ return getProperty(taskName, "threadCount", Integer.class, 0);
+ }
+
+ @Override
+ public String getDomain(String taskName) {
+ return getProperty(taskName, "domain", String.class, null);
+ }
+
+ private T getProperty(String taskName, String property, Class type, T defaultValue) {
+ String key = "conductor.worker." + taskName + "." + property;
+ T value = environment.getProperty(key, type, defaultValue);
+ if (value == defaultValue) {
+ key = "conductor.worker.all." + property;
+ value = environment.getProperty(key, type, defaultValue);
+ }
+ return value;
+ }
+}
diff --git a/conductor-clients/java/conductor-java-sdk/conductor-client/build.gradle b/conductor-clients/java/conductor-java-sdk/conductor-client/build.gradle
new file mode 100644
index 000000000..9839d0327
--- /dev/null
+++ b/conductor-clients/java/conductor-java-sdk/conductor-client/build.gradle
@@ -0,0 +1,107 @@
+plugins {
+ id 'java-library'
+ id 'idea'
+ id 'maven-publish'
+ id 'signing'
+ id 'com.github.johnrengelman.shadow' version '8.1.1'
+ id 'groovy'
+}
+
+dependencies {
+ implementation "com.squareup.okhttp3:okhttp:${versions.okHttp}"
+
+ // test dependencies
+ testImplementation "org.junit.jupiter:junit-jupiter-api:${versions.junit}"
+ testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${versions.junit}"
+
+ testImplementation "org.powermock:powermock-module-junit4:2.0.9"
+ testImplementation "org.powermock:powermock-api-mockito2:2.0.9"
+
+ testImplementation 'org.spockframework:spock-core:2.3-groovy-3.0'
+ testImplementation 'org.codehaus.groovy:groovy:3.0.15'
+ testImplementation 'ch.qos.logback:logback-classic:1.5.6'
+}
+
+java {
+ withSourcesJar()
+ withJavadocJar()
+}
+
+publishing {
+ publications {
+ mavenJava(MavenPublication) {
+ from components.java
+ pom {
+ name = 'Conductor Client'
+ description = 'Conductor OSS client (http)'
+ url = 'https://github.com/conductor-oss/conductor.git'
+ scm {
+ connection = 'scm:git:git://github.com/conductor-oss/conductor.git'
+ developerConnection = 'scm:git:ssh://github.com/conductor-oss/conductor.git'
+ url = 'https://github.com/conductor-oss/conductor.git'
+ }
+ licenses {
+ license {
+ name = 'The Apache License, Version 2.0'
+ url = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
+ }
+ }
+ developers {
+ developer {
+ organization = 'Orkes'
+ organizationUrl = 'https://orkes.io'
+ name = 'Orkes Development Team'
+ email = 'developers@orkes.io'
+ }
+ }
+ }
+ }
+ }
+
+ repositories {
+ maven {
+ if (project.hasProperty("mavenCentral")) {
+ println "Publishing to Sonatype Repository"
+ url = "https://s01.oss.sonatype.org/${project.version.endsWith('-SNAPSHOT') ? "content/repositories/snapshots/" : "service/local/staging/deploy/maven2/"}"
+ credentials {
+ username project.properties.username
+ password project.properties.password
+ }
+ } else {
+ url = "s3://orkes-artifacts-repo/${project.version.endsWith('-SNAPSHOT') ? "snapshots" : "releases"}"
+ authentication {
+ awsIm(AwsImAuthentication)
+ }
+ }
+ }
+ }
+}
+
+signing {
+ def signingKeyId = findProperty('signingKeyId')
+ if (signingKeyId) {
+ println 'Signing the artifact with keys'
+ signing {
+ def signingKey = findProperty('signingKey')
+ def signingPassword = findProperty('signingPassword')
+ if (signingKeyId && signingKey && signingPassword) {
+ useInMemoryPgpKeys(signingKeyId, signingKey, signingPassword)
+ }
+
+ sign publishing.publications
+ }
+ }
+}
+
+test {
+ useJUnitPlatform()
+}
+
+shadowJar {
+ archiveFileName = "orkes-conductor-client-$version-all.jar"
+ mergeServiceFiles()
+}
+
+tasks.build {
+ dependsOn shadowJar
+}
diff --git a/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/automator/TaskRunner.java b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/automator/TaskRunner.java
new file mode 100644
index 000000000..3c3d9bfbd
--- /dev/null
+++ b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/automator/TaskRunner.java
@@ -0,0 +1,418 @@
+/*
+ * Copyright 2022 Conductor 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 com.netflix.conductor.client.automator;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.apache.commons.lang3.concurrent.BasicThreadFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.netflix.conductor.client.automator.events.PollCompleted;
+import com.netflix.conductor.client.automator.events.PollFailure;
+import com.netflix.conductor.client.automator.events.PollStarted;
+import com.netflix.conductor.client.automator.events.TaskExecutionCompleted;
+import com.netflix.conductor.client.automator.events.TaskExecutionFailure;
+import com.netflix.conductor.client.automator.events.TaskExecutionStarted;
+import com.netflix.conductor.client.automator.events.TaskRunnerEvent;
+import com.netflix.conductor.client.automator.filters.PollFilter;
+import com.netflix.conductor.client.config.PropertyFactory;
+import com.netflix.conductor.client.http.TaskClient;
+import com.netflix.conductor.client.worker.Worker;
+import com.netflix.conductor.common.metadata.tasks.Task;
+import com.netflix.conductor.common.metadata.tasks.TaskResult;
+
+import com.google.common.base.Stopwatch;
+import com.google.common.util.concurrent.Uninterruptibles;
+
+class TaskRunner {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(TaskRunner.class);
+ private final TaskClient taskClient;
+ private final int updateRetryCount;
+ private final ExecutorService executorService;
+ private final int taskPollTimeout;
+ private final Semaphore permits;
+ private final Worker worker;
+ private final int pollingIntervalInMillis;
+ private final String taskType;
+ private final int errorAt;
+ private int pollingErrorCount;
+ private String domain;
+ private volatile boolean pollingAndExecuting = true;
+ private final List pollFilters;
+ private final Map, List>> listeners;
+
+ TaskRunner(Worker worker,
+ TaskClient taskClient,
+ int updateRetryCount,
+ Map taskToDomain,
+ String workerNamePrefix,
+ int threadCount,
+ int taskPollTimeout,
+ List pollFilters,
+ Map, List>> listeners) {
+ this.worker = worker;
+ this.taskClient = taskClient;
+ this.updateRetryCount = updateRetryCount;
+ this.taskPollTimeout = taskPollTimeout;
+ this.pollingIntervalInMillis = worker.getPollingInterval();
+ this.taskType = worker.getTaskDefName();
+ this.permits = new Semaphore(threadCount);
+ this.pollFilters = pollFilters;
+ this.listeners = listeners;
+
+ //1. Is there a worker level override?
+ this.domain = PropertyFactory.getString(taskType, Worker.PROP_DOMAIN, null);
+ if (this.domain == null) {
+ //2. If not, is there a blanket override?
+ this.domain = PropertyFactory.getString(Worker.PROP_ALL_WORKERS, Worker.PROP_DOMAIN, null);
+ }
+ if (this.domain == null) {
+ //3. was it supplied as part of the config?
+ this.domain = taskToDomain.get(taskType);
+ }
+
+ int defaultLoggingInterval = 100;
+ int errorInterval = PropertyFactory.getInteger(taskType, Worker.PROP_LOG_INTERVAL, 0);
+ if (errorInterval == 0) {
+ errorInterval = PropertyFactory.getInteger(Worker.PROP_ALL_WORKERS, Worker.PROP_LOG_INTERVAL, 0);
+ }
+ if (errorInterval == 0) {
+ errorInterval = defaultLoggingInterval;
+ }
+ this.errorAt = errorInterval;
+ LOGGER.info("Polling errors will be sampled at every {} error (after the first 100 errors) for taskType {}", this.errorAt, taskType);
+ this.executorService = Executors.newFixedThreadPool(threadCount,
+ new BasicThreadFactory.Builder()
+ .namingPattern(workerNamePrefix)
+ .uncaughtExceptionHandler(uncaughtExceptionHandler)
+ .build());
+ LOGGER.info("Starting Worker for taskType '{}' with {} threads, {} ms polling interval and domain {}",
+ taskType,
+ threadCount,
+ pollingIntervalInMillis,
+ domain);
+ LOGGER.info("Polling errors for taskType {} will be printed at every {} occurrence.", taskType, errorAt);
+ }
+
+ public void pollAndExecute() {
+ Stopwatch stopwatch = null;
+ while (pollingAndExecuting) {
+ if (Thread.currentThread().isInterrupted()) {
+ break; // Exit the loop if interrupted
+ }
+
+ try {
+ List tasks = pollTasksForWorker();
+ if (tasks.isEmpty()) {
+ if (stopwatch == null) {
+ stopwatch = Stopwatch.createStarted();
+ }
+ Uninterruptibles.sleepUninterruptibly(pollingIntervalInMillis, TimeUnit.MILLISECONDS);
+ continue;
+ }
+ if (stopwatch != null) {
+ stopwatch.stop();
+ LOGGER.trace("Poller for task {} waited for {} ms before getting {} tasks to execute", taskType, stopwatch.elapsed(TimeUnit.MILLISECONDS), tasks.size());
+ stopwatch = null;
+ }
+ tasks.forEach(task -> this.executorService.submit(() -> this.processTask(task)));
+ } catch (Throwable t) {
+ LOGGER.error(t.getMessage(), t);
+ }
+ }
+ }
+
+ public void shutdown(int timeout) {
+ try {
+ pollingAndExecuting = false;
+ executorService.shutdown();
+ if (executorService.awaitTermination(timeout, TimeUnit.SECONDS)) {
+ LOGGER.debug("tasks completed, shutting down");
+ } else {
+ LOGGER.warn(String.format("forcing shutdown after waiting for %s second", timeout));
+ executorService.shutdownNow();
+ }
+ } catch (InterruptedException ie) {
+ LOGGER.warn("shutdown interrupted, invoking shutdownNow");
+ executorService.shutdownNow();
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ private List pollTasksForWorker() {
+ publish(new PollStarted(taskType));
+
+ if (worker.paused()) {
+ LOGGER.trace("Worker {} has been paused. Not polling anymore!", worker.getClass());
+ return List.of();
+ }
+
+ for (PollFilter filter : pollFilters) {
+ if (!filter.filter(taskType, domain)) {
+ LOGGER.trace("Filter returned false, not polling.");
+ return List.of();
+ }
+ }
+
+ int pollCount = 0;
+ while (permits.tryAcquire()) {
+ pollCount++;
+ }
+
+ if (pollCount == 0) {
+ return List.of();
+ }
+
+ List tasks = new LinkedList<>();
+ Stopwatch stopwatch = Stopwatch.createStarted(); //TODO move this to the top?
+ try {
+ LOGGER.trace("Polling task of type: {} in domain: '{}' with size {}", taskType, domain, pollCount);
+ tasks = pollTask(domain, pollCount);
+ permits.release(pollCount - tasks.size()); //release extra permits
+ stopwatch.stop();
+ long elapsed = stopwatch.elapsed(TimeUnit.MILLISECONDS);
+ LOGGER.debug("Time taken to poll {} task with a batch size of {} is {} ms", taskType, tasks.size(), elapsed);
+ publish(new PollCompleted(taskType, elapsed));
+ } catch (Throwable e) {
+ permits.release(pollCount - tasks.size());
+
+ //For the first 100 errors, just print them as is...
+ boolean printError = pollingErrorCount < 100 || pollingErrorCount % errorAt == 0;
+ pollingErrorCount++;
+ if (pollingErrorCount > 10_000_000) {
+ //Reset after 10 million errors
+ pollingErrorCount = 0;
+ }
+ if (printError) {
+ LOGGER.error("Error polling for taskType: {}, error = {}", taskType, e.getMessage(), e);
+ }
+
+ if (stopwatch.isRunning()) {
+ stopwatch.stop();
+ }
+
+ long elapsed = stopwatch.elapsed(TimeUnit.MILLISECONDS);
+ publish(new PollFailure(taskType, elapsed, e));
+ }
+
+ return tasks;
+ }
+
+ private List pollTask(String domain, int count) {
+ if (count < 1) {
+ return Collections.emptyList();
+ }
+ String workerId = worker.getIdentity();
+ LOGGER.debug("poll {} in the domain {} with batch size {}", taskType, domain, count);
+ return taskClient.batchPollTasksInDomain(
+ taskType, domain, workerId, count, this.taskPollTimeout);
+ }
+
+ @SuppressWarnings("FieldCanBeLocal")
+ private final Thread.UncaughtExceptionHandler uncaughtExceptionHandler =
+ (thread, error) -> {
+ // JVM may be in unstable state, try to send metrics then exit
+ LOGGER.error("Uncaught exception. Thread {} will exit now", thread, error);
+ };
+
+ private void processTask(Task task) {
+ publish(new TaskExecutionStarted(taskType, task.getTaskId(), worker.getIdentity()));
+ LOGGER.trace("Executing task: {} of type: {} in worker: {} at {}", task.getTaskId(), taskType, worker.getClass().getSimpleName(), worker.getIdentity());
+ LOGGER.trace("task {} is getting executed after {} ms of getting polled", task.getTaskId(), (System.currentTimeMillis() - task.getStartTime()));
+ Stopwatch stopwatch = Stopwatch.createStarted();
+ try {
+ executeTask(worker, task);
+ stopwatch.stop();
+ long elapsed = stopwatch.elapsed(TimeUnit.MILLISECONDS);
+ LOGGER.trace(
+ "Took {} ms to execute and update task with id {}",
+ elapsed,
+ task.getTaskId());
+ } catch (Throwable t) {
+ task.setStatus(Task.Status.FAILED);
+ TaskResult result = new TaskResult(task);
+ handleException(t, result, worker, task);
+ } finally {
+ permits.release();
+ }
+ }
+
+ private void executeTask(Worker worker, Task task) {
+ if (task == null || task.getTaskDefName().isEmpty()) {
+ LOGGER.warn("Empty task {}", worker.getTaskDefName());
+ return;
+ }
+
+ Stopwatch stopwatch = Stopwatch.createStarted();
+ TaskResult result = null;
+ try {
+ LOGGER.trace(
+ "Executing task: {} in worker: {} at {}",
+ task.getTaskId(),
+ worker.getClass().getSimpleName(),
+ worker.getIdentity());
+ result = worker.execute(task);
+ stopwatch.stop();
+ publish(new TaskExecutionCompleted(taskType, task.getTaskId(), worker.getIdentity(), stopwatch.elapsed(TimeUnit.MILLISECONDS)));
+ result.setWorkflowInstanceId(task.getWorkflowInstanceId());
+ result.setTaskId(task.getTaskId());
+ result.setWorkerId(worker.getIdentity());
+ } catch (Exception e) {
+ if (stopwatch.isRunning()) {
+ stopwatch.stop();
+ }
+ publish(new TaskExecutionFailure(taskType, task.getTaskId(), worker.getIdentity(), e, stopwatch.elapsed(TimeUnit.MILLISECONDS)));
+
+ LOGGER.error(
+ "Unable to execute task: {} of type: {}",
+ task.getTaskId(),
+ task.getTaskDefName(),
+ e);
+ if (result == null) {
+ task.setStatus(Task.Status.FAILED);
+ result = new TaskResult(task);
+ }
+ handleException(e, result, worker, task);
+ }
+
+ LOGGER.trace(
+ "Task: {} executed by worker: {} at {} with status: {}",
+ task.getTaskId(),
+ worker.getClass().getSimpleName(),
+ worker.getIdentity(),
+ result.getStatus());
+ Stopwatch updateStopWatch = Stopwatch.createStarted();
+ updateTaskResult(updateRetryCount, task, result, worker);
+ updateStopWatch.stop();
+ LOGGER.trace(
+ "Time taken to update the {} {} ms",
+ task.getTaskType(),
+ updateStopWatch.elapsed(TimeUnit.MILLISECONDS));
+ }
+
+ private void updateTaskResult(int count, Task task, TaskResult result, Worker worker) {
+ try {
+ // upload if necessary
+ Optional optionalExternalStorageLocation =
+ retryOperation(
+ (TaskResult taskResult) -> upload(taskResult, task.getTaskType()),
+ count,
+ result,
+ "evaluateAndUploadLargePayload");
+
+ if (optionalExternalStorageLocation.isPresent()) {
+ result.setExternalOutputPayloadStoragePath(optionalExternalStorageLocation.get());
+ result.setOutputData(null);
+ }
+
+ retryOperation(
+ (TaskResult taskResult) -> {
+ taskClient.updateTask(taskResult);
+ return null;
+ },
+ count,
+ result,
+ "updateTask");
+ } catch (Exception e) {
+ worker.onErrorUpdate(task);
+ LOGGER.error(
+ String.format(
+ "Failed to update result: %s for task: %s in worker: %s",
+ result.toString(), task.getTaskDefName(), worker.getIdentity()),
+ e);
+ }
+ }
+
+ //FIXME
+ private Optional upload(TaskResult result, String taskType) {
+ // do nothing
+ return Optional.empty();
+ }
+
+ private R retryOperation(Function operation, int count, T input, String opName) {
+ int index = 0;
+ while (index < count) {
+ try {
+ return operation.apply(input);
+ } catch (Exception e) {
+ LOGGER.error("Error executing {}", opName, e);
+ index++;
+ Uninterruptibles.sleepUninterruptibly(500L * (count + 1), TimeUnit.MILLISECONDS);
+ }
+ }
+ throw new RuntimeException("Exhausted retries performing " + opName);
+ }
+
+ private void publish(TaskRunnerEvent event) {
+ if (noListeners(event)) {
+ return;
+ }
+
+ CompletableFuture.runAsync(() -> {
+ List> eventListeners = getEventListeners(event);
+ for (Consumer extends TaskRunnerEvent> listener : eventListeners) {
+ ((Consumer) listener).accept(event);
+ }
+ });
+ }
+
+ private boolean noListeners(TaskRunnerEvent event) {
+ List> specificEventListeners = this.listeners.get(event.getClass());
+ List> promiscuousListeners = this.listeners.get(TaskRunnerEvent.class);
+
+ return (specificEventListeners == null || specificEventListeners.isEmpty())
+ && (promiscuousListeners == null || promiscuousListeners.isEmpty());
+ }
+
+ private List> getEventListeners(TaskRunnerEvent event) {
+ List> specificEventListeners = this.listeners.get(event.getClass());
+ List> promiscuousListeners = this.listeners.get(TaskRunnerEvent.class);
+ if (promiscuousListeners == null || promiscuousListeners.isEmpty()) {
+ return specificEventListeners;
+ }
+
+ if (specificEventListeners == null || specificEventListeners.isEmpty()) {
+ return promiscuousListeners;
+ }
+
+ return Stream.concat(specificEventListeners.stream(), promiscuousListeners.stream())
+ .collect(Collectors.toList());
+ }
+
+ private void handleException(Throwable t, TaskResult result, Worker worker, Task task) {
+ LOGGER.error(String.format("Error while executing task %s", task.toString()), t);
+ result.setStatus(TaskResult.Status.FAILED);
+ result.setReasonForIncompletion("Error while executing the task: " + t);
+ StringWriter stringWriter = new StringWriter();
+ t.printStackTrace(new PrintWriter(stringWriter));
+ result.log(stringWriter.toString());
+ updateTaskResult(updateRetryCount, task, result, worker);
+ }
+}
diff --git a/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/automator/TaskRunnerConfigurer.java b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/automator/TaskRunnerConfigurer.java
new file mode 100644
index 000000000..a04acd622
--- /dev/null
+++ b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/automator/TaskRunnerConfigurer.java
@@ -0,0 +1,354 @@
+/*
+ * Copyright 2022 Conductor 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 com.netflix.conductor.client.automator;
+
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.function.Consumer;
+
+import org.apache.commons.lang3.concurrent.BasicThreadFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.netflix.conductor.client.automator.events.PollCompleted;
+import com.netflix.conductor.client.automator.events.PollFailure;
+import com.netflix.conductor.client.automator.events.PollStarted;
+import com.netflix.conductor.client.automator.events.TaskExecutionCompleted;
+import com.netflix.conductor.client.automator.events.TaskExecutionFailure;
+import com.netflix.conductor.client.automator.events.TaskExecutionStarted;
+import com.netflix.conductor.client.automator.events.TaskRunnerEvent;
+import com.netflix.conductor.client.automator.filters.PollFilter;
+import com.netflix.conductor.client.config.ConductorClientConfiguration;
+import com.netflix.conductor.client.http.TaskClient;
+import com.netflix.conductor.client.metrics.MetricsCollector;
+import com.netflix.conductor.client.worker.Worker;
+
+import com.google.common.base.Preconditions;
+
+public class TaskRunnerConfigurer {
+ private static final Logger LOGGER = LoggerFactory.getLogger(TaskRunnerConfigurer.class);
+ private final TaskClient taskClient;
+ private final List workers;
+ private final int sleepWhenRetry;
+ private final int updateRetryCount;
+ private final int shutdownGracePeriodSeconds;
+ private final String workerNamePrefix;
+ private final Map taskToDomain;
+ private final Map taskToThreadCount;
+ private final Map taskPollTimeout;
+ private final Map taskPollCount;
+ private final Integer defaultPollTimeout;
+ private final int threadCount;
+ private final List taskRunners;
+ private ScheduledExecutorService scheduledExecutorService;
+ private final List pollFilters;
+ private final Map, List>> listeners;
+
+ /**
+ * @see TaskRunnerConfigurer.Builder
+ * @see TaskRunnerConfigurer#init()
+ */
+ private TaskRunnerConfigurer(TaskRunnerConfigurer.Builder builder) {
+ this.taskClient = builder.taskClient;
+ this.sleepWhenRetry = builder.sleepWhenRetry;
+ this.updateRetryCount = builder.updateRetryCount;
+ this.workerNamePrefix = builder.workerNamePrefix;
+ this.taskToDomain = builder.taskToDomain;
+ this.taskToThreadCount = builder.taskToThreadCount;
+ this.taskPollTimeout = builder.taskPollTimeout;
+ this.taskPollCount = builder.taskPollCount;
+ this.defaultPollTimeout = builder.defaultPollTimeout;
+ this.shutdownGracePeriodSeconds = builder.shutdownGracePeriodSeconds;
+ this.workers = new LinkedList<>();
+ this.threadCount = builder.threadCount;
+ this.pollFilters = builder.pollFilters;
+ this.listeners = builder.listeners;
+ builder.workers.forEach(this.workers::add);
+ taskRunners = new LinkedList<>();
+ }
+
+ /**
+ * @return Thread Count for the shared executor pool
+ */
+ @Deprecated
+ public int getThreadCount() {
+ return this.threadCount;
+ }
+
+ /**
+ * @return Thread Count for individual task type
+ */
+ public Map getTaskThreadCount() {
+ return taskToThreadCount;
+ }
+
+ /**
+ * @return seconds before forcing shutdown of worker
+ */
+ public int getShutdownGracePeriodSeconds() {
+ return shutdownGracePeriodSeconds;
+ }
+
+ /**
+ * @return sleep time in millisecond before task update retry is done when receiving error from
+ * the Conductor server
+ */
+ public int getSleepWhenRetry() {
+ return sleepWhenRetry;
+ }
+
+ /**
+ * @return Number of times updateTask should be retried when receiving error from Conductor
+ * server
+ */
+ public int getUpdateRetryCount() {
+ return updateRetryCount;
+ }
+
+ /**
+ * @return prefix used for worker names
+ */
+ public String getWorkerNamePrefix() {
+ return workerNamePrefix;
+ }
+
+ /**
+ * Starts the polling. Must be called after {@link TaskRunnerConfigurer.Builder#build()} method.
+ */
+ public synchronized void init() {
+ this.scheduledExecutorService = Executors.newScheduledThreadPool(workers.size(),
+ new BasicThreadFactory.Builder()
+ .namingPattern("TaskRunner %d")
+ .build());
+ workers.forEach(worker -> scheduledExecutorService.submit(() -> this.startWorker(worker)));
+ }
+
+ /**
+ * Invoke this method within a PreDestroy block within your application to facilitate a graceful
+ * shutdown of your worker, during process termination.
+ */
+ public void shutdown() {
+ if (taskRunners != null) {
+ synchronized (taskRunners) {
+ taskRunners.forEach(taskRunner -> taskRunner.shutdown(shutdownGracePeriodSeconds));
+ }
+ }
+ scheduledExecutorService.shutdown();
+ }
+
+ private void startWorker(Worker worker) {
+ final Integer threadCountForTask = this.taskToThreadCount.getOrDefault(worker.getTaskDefName(), threadCount);
+ final Integer taskPollTimeout = this.taskPollTimeout.getOrDefault(worker.getTaskDefName(), defaultPollTimeout);
+ LOGGER.info("Domain map for tasks = {}", taskToDomain);
+ final TaskRunner taskRunner = new TaskRunner(
+ worker,
+ taskClient,
+ updateRetryCount,
+ taskToDomain,
+ workerNamePrefix,
+ threadCountForTask,
+ taskPollTimeout,
+ pollFilters,
+ listeners);
+ // startWorker(worker) is executed by several threads.
+ // taskRunners.add(taskRunner) without synchronization could lead to a race condition and unpredictable behavior,
+ // including potential null values being inserted or corrupted state.
+ synchronized (taskRunners) {
+ taskRunners.add(taskRunner);
+ }
+
+ taskRunner.pollAndExecute();
+ }
+
+ /**
+ * Builder used to create the instances of TaskRunnerConfigurer
+ */
+ public static class Builder {
+
+ private String workerNamePrefix = "workflow-worker-%d";
+ private int sleepWhenRetry = 500;
+ private int updateRetryCount = 3;
+ private int threadCount = -1;
+ private int shutdownGracePeriodSeconds = 10;
+ private int defaultPollTimeout = 100;
+ private int defaultPollCount = 20;
+ private final Iterable workers;
+ private final TaskClient taskClient;
+ private Map taskToDomain = new HashMap<>();
+ private Map taskToThreadCount =
+ new HashMap<>();
+ private Map taskPollTimeout = new HashMap<>();
+ private Map taskPollCount = new HashMap<>();
+ private final List pollFilters = new LinkedList<>();
+ private final Map, List>> listeners = new HashMap<>();
+
+ public Builder(TaskClient taskClient, Iterable workers) {
+ Preconditions.checkNotNull(taskClient, "TaskClient cannot be null");
+ Preconditions.checkNotNull(workers, "Workers cannot be null");
+ this.taskClient = taskClient;
+ this.workers = workers;
+ }
+
+ /**
+ * @param workerNamePrefix prefix to be used for worker names, defaults to workflow-worker-
+ * if not supplied.
+ * @return Returns the current instance.
+ */
+ public TaskRunnerConfigurer.Builder withWorkerNamePrefix(String workerNamePrefix) {
+ this.workerNamePrefix = workerNamePrefix;
+ return this;
+ }
+
+ /**
+ * @param sleepWhenRetry time in milliseconds, for which the thread should sleep when task
+ * update call fails, before retrying the operation.
+ * @return Returns the current instance.
+ */
+ public TaskRunnerConfigurer.Builder withSleepWhenRetry(int sleepWhenRetry) {
+ this.sleepWhenRetry = sleepWhenRetry;
+ return this;
+ }
+
+ /**
+ * @param updateRetryCount number of times to retry the failed updateTask operation
+ * @return Builder instance
+ * @see #withSleepWhenRetry(int)
+ */
+ public TaskRunnerConfigurer.Builder withUpdateRetryCount(int updateRetryCount) {
+ this.updateRetryCount = updateRetryCount;
+ return this;
+ }
+
+ /**
+ * @param conductorClientConfiguration client configuration to handle external payloads
+ * @return Builder instance
+ */
+ public TaskRunnerConfigurer.Builder withConductorClientConfiguration(
+ ConductorClientConfiguration conductorClientConfiguration) {
+ return this;
+ }
+
+ /**
+ * @param shutdownGracePeriodSeconds waiting seconds before forcing shutdown of your worker
+ * @return Builder instance
+ */
+ public TaskRunnerConfigurer.Builder withShutdownGracePeriodSeconds(
+ int shutdownGracePeriodSeconds) {
+ if (shutdownGracePeriodSeconds < 1) {
+ throw new IllegalArgumentException(
+ "Seconds of shutdownGracePeriod cannot be less than 1");
+ }
+ this.shutdownGracePeriodSeconds = shutdownGracePeriodSeconds;
+ return this;
+ }
+
+ public TaskRunnerConfigurer.Builder withTaskToDomain(Map taskToDomain) {
+ this.taskToDomain = taskToDomain;
+ return this;
+ }
+
+ public TaskRunnerConfigurer.Builder withTaskThreadCount(
+ Map taskToThreadCount) {
+ this.taskToThreadCount = taskToThreadCount;
+ return this;
+ }
+
+ public TaskRunnerConfigurer.Builder withTaskToThreadCount(
+ Map taskToThreadCount) {
+ this.taskToThreadCount = taskToThreadCount;
+ return this;
+ }
+
+ public TaskRunnerConfigurer.Builder withTaskPollTimeout(
+ Map taskPollTimeout) {
+ this.taskPollTimeout = taskPollTimeout;
+ return this;
+ }
+
+ public TaskRunnerConfigurer.Builder withTaskPollTimeout(Integer taskPollTimeout) {
+ this.defaultPollTimeout = taskPollTimeout;
+ return this;
+ }
+
+ public TaskRunnerConfigurer.Builder withTaskPollCount(Map taskPollCount) {
+ this.taskPollCount = taskPollCount;
+ return this;
+ }
+
+ public TaskRunnerConfigurer.Builder withTaskPollCount(int defaultPollCount) {
+ this.defaultPollCount = defaultPollCount;
+ return this;
+ }
+
+ /**
+ * Builds an instance of the TaskRunnerConfigurer.
+ *
+ * Please see {@link TaskRunnerConfigurer#init()} method. The method must be called after
+ * this constructor for the polling to start.
+ *
+ * @return Builder instance
+ */
+ public TaskRunnerConfigurer build() {
+ return new TaskRunnerConfigurer(this);
+ }
+
+ /**
+ * @param threadCount # of threads assigned to the workers. Should be at-least the size of
+ * taskWorkers to avoid starvation in a busy system.
+ * @return Builder instance
+ */
+ public Builder withThreadCount(int threadCount) {
+ if (threadCount < 1) {
+ throw new IllegalArgumentException("No. of threads cannot be less than 1");
+ }
+ this.threadCount = threadCount;
+ return this;
+ }
+
+ public Builder withPollFilter(PollFilter filter) {
+ pollFilters.add(filter);
+ return this;
+ }
+
+ public Builder withListener(Class eventType, Consumer listener) {
+ listeners.computeIfAbsent(eventType, k -> new LinkedList<>()).add(listener);
+ return this;
+ }
+
+ public Builder withMetricsCollector(MetricsCollector metricsCollector) {
+ listeners.computeIfAbsent(PollFailure.class, k -> new LinkedList<>())
+ .add((Consumer) metricsCollector::consume);
+
+ listeners.computeIfAbsent(PollCompleted.class, k -> new LinkedList<>())
+ .add((Consumer) metricsCollector::consume);
+
+ listeners.computeIfAbsent(PollStarted.class, k -> new LinkedList<>())
+ .add((Consumer) metricsCollector::consume);
+
+ listeners.computeIfAbsent(TaskExecutionStarted.class, k -> new LinkedList<>())
+ .add((Consumer) metricsCollector::consume);
+
+ listeners.computeIfAbsent(TaskExecutionCompleted.class, k -> new LinkedList<>())
+ .add((Consumer) metricsCollector::consume);
+
+ listeners.computeIfAbsent(TaskExecutionFailure.class, k -> new LinkedList<>())
+ .add((Consumer) metricsCollector::consume);
+
+ return this;
+ }
+ }
+}
diff --git a/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/automator/events/PollCompleted.java b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/automator/events/PollCompleted.java
new file mode 100644
index 000000000..15274c1b9
--- /dev/null
+++ b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/automator/events/PollCompleted.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2024 Conductor 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 com.netflix.conductor.client.automator.events;
+
+import java.time.Duration;
+
+import lombok.Getter;
+import lombok.ToString;
+
+@Getter
+@ToString
+public final class PollCompleted extends TaskRunnerEvent {
+ private final Duration duration;
+
+ public PollCompleted(String taskType, long durationInMillis) {
+ super(taskType);
+ this.duration = Duration.ofMillis(durationInMillis);
+ }
+}
diff --git a/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/automator/events/PollFailure.java b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/automator/events/PollFailure.java
new file mode 100644
index 000000000..3a5d375dd
--- /dev/null
+++ b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/automator/events/PollFailure.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2024 Conductor 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 com.netflix.conductor.client.automator.events;
+
+import java.time.Duration;
+
+import lombok.Getter;
+import lombok.ToString;
+
+@Getter
+@ToString
+public final class PollFailure extends TaskRunnerEvent {
+ private final Duration duration;
+ private final Throwable cause;
+
+ public PollFailure(String taskType, long durationInMillis, Throwable cause) {
+ super(taskType);
+ this.duration = Duration.ofMillis(durationInMillis);
+ this.cause = cause;
+ }
+}
diff --git a/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/automator/events/PollStarted.java b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/automator/events/PollStarted.java
new file mode 100644
index 000000000..62068bfb0
--- /dev/null
+++ b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/automator/events/PollStarted.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2024 Conductor 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 com.netflix.conductor.client.automator.events;
+
+import lombok.ToString;
+
+@ToString
+public final class PollStarted extends TaskRunnerEvent {
+
+ public PollStarted(String taskType) {
+ super(taskType);
+ }
+}
diff --git a/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/automator/events/TaskExecutionCompleted.java b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/automator/events/TaskExecutionCompleted.java
new file mode 100644
index 000000000..2ca962d1c
--- /dev/null
+++ b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/automator/events/TaskExecutionCompleted.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2024 Conductor 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 com.netflix.conductor.client.automator.events;
+
+import java.time.Duration;
+
+import lombok.Getter;
+import lombok.ToString;
+
+@Getter
+@ToString
+public final class TaskExecutionCompleted extends TaskRunnerEvent {
+ public final String taskId;
+ public final String workerId;
+ private final Duration duration;
+
+ public TaskExecutionCompleted(String taskType, String taskId, String workerId, long durationInMillis) {
+ super(taskType);
+ this.taskId = taskId;
+ this.workerId = workerId;
+ this.duration = Duration.ofMillis(durationInMillis);
+ }
+}
diff --git a/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/automator/events/TaskExecutionFailure.java b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/automator/events/TaskExecutionFailure.java
new file mode 100644
index 000000000..3ec43c7b2
--- /dev/null
+++ b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/automator/events/TaskExecutionFailure.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2024 Conductor 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 com.netflix.conductor.client.automator.events;
+
+import java.time.Duration;
+
+import lombok.Getter;
+import lombok.ToString;
+
+@Getter
+@ToString
+public final class TaskExecutionFailure extends TaskRunnerEvent {
+ public final String taskId;
+ public final String workerId;
+ private final Duration duration;
+ private final Throwable cause;
+
+ public TaskExecutionFailure(String taskType, String taskId, String workerId, Throwable cause, long durationInMillis) {
+ super(taskType);
+ this.cause = cause;
+ this.taskId = taskId;
+ this.workerId = workerId;
+ this.duration = Duration.ofMillis(durationInMillis);
+ }
+}
diff --git a/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/automator/events/TaskExecutionStarted.java b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/automator/events/TaskExecutionStarted.java
new file mode 100644
index 000000000..6ab388653
--- /dev/null
+++ b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/automator/events/TaskExecutionStarted.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2024 Conductor 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 com.netflix.conductor.client.automator.events;
+
+import lombok.Getter;
+import lombok.ToString;
+
+@Getter
+@ToString
+public final class TaskExecutionStarted extends TaskRunnerEvent {
+ public final String taskId;
+ public final String workerId;
+
+ public TaskExecutionStarted(String taskType, String taskId, String workerId) {
+ super(taskType);
+ this.taskId = taskId;
+ this.workerId = workerId;
+ }
+}
diff --git a/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/automator/events/TaskRunnerEvent.java b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/automator/events/TaskRunnerEvent.java
new file mode 100644
index 000000000..48474f134
--- /dev/null
+++ b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/automator/events/TaskRunnerEvent.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2024 Conductor 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 com.netflix.conductor.client.automator.events;
+
+import java.time.Instant;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.ToString;
+
+@AllArgsConstructor
+@Getter
+@ToString
+public abstract class TaskRunnerEvent {
+ private final Instant time = Instant.now();
+ private final String taskType;
+}
diff --git a/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/automator/filters/PollFilter.java b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/automator/filters/PollFilter.java
new file mode 100644
index 000000000..d01275e04
--- /dev/null
+++ b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/automator/filters/PollFilter.java
@@ -0,0 +1,18 @@
+/*
+ * Copyright 2024 Conductor 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 com.netflix.conductor.client.automator.filters;
+
+@FunctionalInterface
+public interface PollFilter {
+ boolean filter(String type, String domain);
+}
diff --git a/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/config/ConductorClientConfiguration.java b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/config/ConductorClientConfiguration.java
new file mode 100644
index 000000000..bb5a28926
--- /dev/null
+++ b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/config/ConductorClientConfiguration.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2018 Conductor 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 com.netflix.conductor.client.config;
+
+public interface ConductorClientConfiguration {
+
+ /**
+ * @return the workflow input payload size threshold in KB, beyond which the payload will be
+ * processed based on {@link
+ * ConductorClientConfiguration#isExternalPayloadStorageEnabled()}.
+ */
+ int getWorkflowInputPayloadThresholdKB();
+
+ /**
+ * @return the max value of workflow input payload size threshold in KB, beyond which the
+ * payload will be rejected regardless external payload storage is enabled.
+ */
+ int getWorkflowInputMaxPayloadThresholdKB();
+
+ /**
+ * @return the task output payload size threshold in KB, beyond which the payload will be
+ * processed based on {@link
+ * ConductorClientConfiguration#isExternalPayloadStorageEnabled()}.
+ */
+ int getTaskOutputPayloadThresholdKB();
+
+ /**
+ * @return the max value of task output payload size threshold in KB, beyond which the payload
+ * will be rejected regardless external payload storage is enabled.
+ */
+ int getTaskOutputMaxPayloadThresholdKB();
+
+ /**
+ * @return the flag which controls the use of external storage for storing workflow/task input
+ * and output JSON payloads with size greater than threshold. If it is set to true, the
+ * payload is stored in external location. If it is set to false, the payload is rejected
+ * and the task/workflow execution fails.
+ */
+ boolean isExternalPayloadStorageEnabled();
+}
diff --git a/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/config/PropertyFactory.java b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/config/PropertyFactory.java
new file mode 100644
index 000000000..5d194794b
--- /dev/null
+++ b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/config/PropertyFactory.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2020 Conductor 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 com.netflix.conductor.client.config;
+
+import java.io.InputStream;
+import java.util.Properties;
+import java.util.concurrent.ConcurrentHashMap;
+
+import lombok.RequiredArgsConstructor;
+import lombok.SneakyThrows;
+
+/**
+ * Used to configure the Conductor workers using conductor-workers.properties.
+ */
+public class PropertyFactory {
+
+ private static final Properties PROPERTIES = loadProperties("conductor-workers.properties");
+
+ private static final String PROPERTY_PREFIX = "conductor.worker";
+
+ // Handle potential parsing exceptions ?
+
+ @RequiredArgsConstructor
+ private static class WorkerProperty {
+
+ final String key;
+
+ String getString() {
+ if (PROPERTIES == null) {
+ return null;
+ }
+
+ return PROPERTIES.getProperty(key);
+ }
+
+ Integer getInteger() {
+ String value = getString();
+ return value == null ? null : Integer.parseInt(value);
+ }
+
+ Boolean getBoolean() {
+ String value = getString();
+ return value == null ? null : Boolean.parseBoolean(value);
+ }
+
+ String getString(String defaultValue) {
+ String value = getString();
+ return value == null ? defaultValue : value;
+ }
+
+ Integer getInteger(Integer defaultValue) {
+ String value = getString();
+ return value == null ? defaultValue : Integer.parseInt(value);
+ }
+
+ Boolean getBoolean(Boolean defaultValue) {
+ String value = getString();
+ return value == null ? defaultValue : Boolean.parseBoolean(value);
+ }
+ }
+
+ private final WorkerProperty global;
+
+ private final WorkerProperty local;
+
+ private static final ConcurrentHashMap PROPERTY_FACTORY_MAP =
+ new ConcurrentHashMap<>();
+
+ private PropertyFactory(String prefix, String propName, String workerName) {
+ this.global = new WorkerProperty(prefix + "." + propName);
+ this.local = new WorkerProperty(prefix + "." + workerName + "." + propName);
+ }
+
+ /**
+ * @param defaultValue Default Value
+ * @return Returns the value as integer. If not value is set (either global or worker specific),
+ * then returns the default value.
+ */
+ public Integer getInteger(int defaultValue) {
+ Integer value = local.getInteger();
+ if (value == null) {
+ value = global.getInteger(defaultValue);
+ }
+ return value;
+ }
+
+ /**
+ * @param defaultValue Default Value
+ * @return Returns the value as String. If not value is set (either global or worker specific),
+ * then returns the default value.
+ */
+ public String getString(String defaultValue) {
+ String value = local.getString();
+ if (value == null) {
+ value = global.getString(defaultValue);
+ }
+ return value;
+ }
+
+ /**
+ * @param defaultValue Default Value
+ * @return Returns the value as Boolean. If not value is set (either global or worker specific),
+ * then returns the default value.
+ */
+ public Boolean getBoolean(Boolean defaultValue) {
+ Boolean value = local.getBoolean();
+ if (value == null) {
+ value = global.getBoolean(defaultValue);
+ }
+ return value;
+ }
+
+ public static Integer getInteger(String workerName, String property, Integer defaultValue) {
+ return getPropertyFactory(workerName, property).getInteger(defaultValue);
+ }
+
+ public static Boolean getBoolean(String workerName, String property, Boolean defaultValue) {
+ return getPropertyFactory(workerName, property).getBoolean(defaultValue);
+ }
+
+ public static String getString(String workerName, String property, String defaultValue) {
+ return getPropertyFactory(workerName, property).getString(defaultValue);
+ }
+
+ private static PropertyFactory getPropertyFactory(String workerName, String property) {
+ String key = property + "." + workerName;
+ return PROPERTY_FACTORY_MAP.computeIfAbsent(
+ key, t -> new PropertyFactory(PROPERTY_PREFIX, property, workerName));
+ }
+
+ @SneakyThrows
+ private static Properties loadProperties(String file) {
+ Properties properties = new Properties();
+ try (InputStream input = PropertyFactory.class.getClassLoader().getResourceAsStream(file)) {
+ if (input == null) {
+ return null;
+ }
+ properties.load(input);
+ }
+
+ return properties;
+ }
+}
diff --git a/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/exception/ConductorClientException.java b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/exception/ConductorClientException.java
new file mode 100644
index 000000000..8be9bbf69
--- /dev/null
+++ b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/exception/ConductorClientException.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright 2022 Conductor 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 com.netflix.conductor.client.exception;
+
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.lang3.StringUtils;
+
+import com.netflix.conductor.common.validation.ValidationError;
+
+public class ConductorClientException extends RuntimeException {
+
+ private int status;
+ private String instance;
+ private String code;
+ private boolean retryable;
+
+ public List getValidationErrors() {
+ return validationErrors;
+ }
+
+ public void setValidationErrors(List validationErrors) {
+ this.validationErrors = validationErrors;
+ }
+
+ private List validationErrors;
+
+ private Map> responseHeaders;
+ private String responseBody;
+
+ public ConductorClientException() {
+ }
+
+ public ConductorClientException(Throwable throwable) {
+ super(throwable.getMessage(), throwable);
+ }
+
+ public ConductorClientException(String message) {
+ super(message);
+ }
+
+ public ConductorClientException(String message,
+ Throwable throwable,
+ int code,
+ Map> responseHeaders,
+ String responseBody) {
+ super(message, throwable);
+ setCode(String.valueOf(code));
+ setStatus(code);
+ this.responseHeaders = responseHeaders;
+ this.responseBody = responseBody;
+ }
+
+ public ConductorClientException(String message,
+ int code,
+ Map> responseHeaders,
+ String responseBody) {
+ this(message, null, code, responseHeaders, responseBody);
+ setCode(String.valueOf(code));
+ setStatus(code);
+ }
+
+ public ConductorClientException(String message,
+ Throwable throwable,
+ int code,
+ Map> responseHeaders) {
+ this(message, throwable, code, responseHeaders, null);
+ setCode(String.valueOf(code));
+ setStatus(code);
+ }
+
+ public ConductorClientException(int code, Map> responseHeaders, String responseBody) {
+ this(null, null, code, responseHeaders, responseBody);
+ setCode(String.valueOf(code));
+ setStatus(code);
+ }
+
+ public ConductorClientException(int code, String message) {
+ super(message);
+ setCode(String.valueOf(code));
+ setStatus(code);
+ }
+
+ public ConductorClientException(int code,
+ String message,
+ Map> responseHeaders,
+ String responseBody) {
+ this(code, message);
+ this.responseHeaders = responseHeaders;
+ this.responseBody = responseBody;
+ setCode(String.valueOf(code));
+ setStatus(code);
+ }
+
+ public boolean isClientError() {
+ return getStatus() > 399 && getStatus() < 499;
+ }
+
+ /**
+ * @return HTTP status code
+ */
+ public int getStatusCode() {
+ return getStatus();
+ }
+
+ /**
+ * Get the HTTP response headers.
+ *
+ * @return A map of list of string
+ */
+ public Map> getResponseHeaders() {
+ return responseHeaders;
+ }
+
+ /**
+ * Get the HTTP response body.
+ *
+ * @return Response body in the form of string
+ */
+ public String getResponseBody() {
+ return responseBody;
+ }
+
+ @Override
+ public String getMessage() {
+ return getStatusCode()
+ + ": "
+ + (StringUtils.isBlank(responseBody) ? super.getMessage() : responseBody);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append(getClass().getName()).append(": ");
+
+ if (getMessage() != null) {
+ builder.append(getMessage());
+ }
+
+ if (status > 0) {
+ builder.append(" {status=").append(status);
+ if (this.code != null) {
+ builder.append(", code='").append(code).append("'");
+ }
+
+ builder.append(", retryable: ").append(retryable);
+ }
+
+ if (this.instance != null) {
+ builder.append(", instance: ").append(instance);
+ }
+
+ if (this.validationErrors != null) {
+ builder.append(", validationErrors: ").append(validationErrors);
+ }
+
+ builder.append("}");
+ return builder.toString();
+ }
+
+ public String getCode() {
+ return code;
+ }
+
+ public void setCode(String code) {
+ this.code = code;
+ }
+
+ public void setStatus(int status) {
+ this.status = status;
+ }
+
+
+ public String getInstance() {
+ return instance;
+ }
+
+ public void setInstance(String instance) {
+ this.instance = instance;
+ }
+
+ public boolean isRetryable() {
+ return retryable;
+ }
+
+ public void setRetryable(boolean retryable) {
+ this.retryable = retryable;
+ }
+
+ public int getStatus() {
+ return this.status;
+ }
+}
diff --git a/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/http/ConductorClient.java b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/http/ConductorClient.java
new file mode 100644
index 000000000..6a0af40f2
--- /dev/null
+++ b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/http/ConductorClient.java
@@ -0,0 +1,577 @@
+/*
+ * Copyright 2022 Conductor 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 com.netflix.conductor.client.http;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Type;
+import java.net.Proxy;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import java.security.SecureRandom;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import javax.net.ssl.X509TrustManager;
+
+import org.apache.commons.lang3.StringUtils;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.netflix.conductor.client.exception.ConductorClientException;
+import com.netflix.conductor.common.config.ObjectMapperProvider;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.SneakyThrows;
+import okhttp3.Call;
+import okhttp3.ConnectionPool;
+import okhttp3.HttpUrl;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import okhttp3.internal.http.HttpMethod;
+
+public class ConductorClient {
+ private static final Logger LOGGER = LoggerFactory.getLogger(ConductorClient.class);
+ protected final OkHttpClient okHttpClient;
+ protected final String basePath;
+ protected final ObjectMapper objectMapper;
+ private final boolean verifyingSsl;
+ private final InputStream sslCaCert;
+ private final KeyManager[] keyManagers;
+ private final List headerSuppliers;
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ @SneakyThrows
+ protected ConductorClient(Builder builder) {
+ builder.validateAndAssignDefaults();
+ final OkHttpClient.Builder okHttpBuilder = builder.okHttpClientBuilder;
+ this.objectMapper = builder.objectMapperSupplier.get();
+ this.basePath = builder.basePath();
+ this.verifyingSsl = builder.verifyingSsl();
+ this.sslCaCert = builder.sslCaCert();
+ this.keyManagers = builder.keyManagers();
+ this.headerSuppliers = builder.headerSupplier();
+
+ if (builder.connectTimeout() > -1) {
+ okHttpBuilder.connectTimeout(builder.connectTimeout(), TimeUnit.MILLISECONDS);
+ }
+
+ if (builder.readTimeout() > -1) {
+ okHttpBuilder.readTimeout(builder.readTimeout(), TimeUnit.MILLISECONDS);
+ }
+
+ if (builder.writeTimeout() > -1) {
+ okHttpBuilder.writeTimeout(builder.writeTimeout(), TimeUnit.MILLISECONDS);
+ }
+
+ if (builder.getProxy() != null) {
+ okHttpBuilder.proxy(builder.getProxy());
+ }
+
+ ConnectionPoolConfig connectionPoolConfig = builder.getConnectionPoolConfig();
+ if (connectionPoolConfig != null) {
+ okHttpBuilder.connectionPool(new ConnectionPool(
+ connectionPoolConfig.getMaxIdleConnections(),
+ connectionPoolConfig.getKeepAliveDuration(),
+ connectionPoolConfig.getTimeUnit()
+ ));
+ }
+
+ if (!verifyingSsl) {
+ unsafeClient(okHttpBuilder);
+ } else if (sslCaCert != null) {
+ trustCertificates(okHttpBuilder);
+ }
+
+ this.okHttpClient = okHttpBuilder.build();
+ this.headerSuppliers.forEach(it -> it.init(this));
+ }
+
+ public ConductorClient() {
+ this(new Builder());
+ }
+
+ public ConductorClient(String basePath) {
+ this(new Builder().basePath(basePath));
+ }
+
+ public String getBasePath() {
+ return basePath;
+ }
+
+ public void shutdown() {
+ okHttpClient.dispatcher().executorService().shutdown();
+ okHttpClient.connectionPool().evictAll();
+ if (okHttpClient.cache() != null) {
+ try {
+ okHttpClient.cache().close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ public ConductorClientResponse execute(ConductorClientRequest req) {
+ return execute(req, null);
+ }
+
+ public ConductorClientResponse execute(ConductorClientRequest req, TypeReference typeReference) {
+ Map headerParams = req.getHeaderParams() == null ? new HashMap<>() : new HashMap<>(req.getHeaderParams());
+ List pathParams = req.getPathParams() == null ? new ArrayList<>() : new ArrayList<>(req.getPathParams());
+ List queryParams = req.getQueryParams() == null ? new ArrayList<>() : new ArrayList<>(req.getQueryParams());
+
+ Request request = buildRequest(req.getMethod().toString(),
+ req.getPath(),
+ pathParams,
+ queryParams,
+ headerParams,
+ req.getBody());
+
+ Call call = okHttpClient.newCall(request);
+ if (typeReference == null) {
+ execute(call, null);
+ return null;
+ }
+
+ return execute(call, typeReference.getType());
+ }
+
+ private String parameterToString(Object param) {
+ if (param == null) {
+ return "";
+ } else if (param instanceof Collection) {
+ StringBuilder b = new StringBuilder();
+ for (Object o : (Collection) param) {
+ if (b.length() > 0) {
+ b.append(",");
+ }
+ b.append(o);
+ }
+ return b.toString();
+ }
+
+ return String.valueOf(param);
+ }
+
+ private boolean isJsonMime(String mime) {
+ String jsonMime = "(?i)^(application/json|[^;/ \t]+/[^;/ \t]+[+]json)[ \t]*(;.*)?$";
+ return mime != null && (mime.matches(jsonMime) || mime.equals("*/*"));
+ }
+
+ private String urlEncode(String str) {
+ return URLEncoder.encode(str, StandardCharsets.UTF_8);
+ }
+
+ @SneakyThrows
+ private T deserialize(Response response, Type returnType) {
+ if (returnType == null) {
+ return null;
+ }
+
+ String body = bodyAsString(response);
+ if (body == null || "".equals(body)) {
+ return null;
+ }
+
+ String contentType = response.header("Content-Type");
+ if (contentType == null || isJsonMime(contentType)) {
+ // This is hacky. It's required because Conductor's API is returning raw strings as JSON
+ if (returnType.equals(String.class)) {
+ //noinspection unchecked
+ return (T) body;
+ }
+
+ JavaType javaType = objectMapper.getTypeFactory().constructType(returnType);
+ return objectMapper.readValue(body, javaType);
+ } else if (returnType.equals(String.class)) {
+ //noinspection unchecked
+ return (T) body;
+ }
+
+ throw new ConductorClientException(
+ "Content type \"" + contentType + "\" is not supported for type: " + returnType,
+ response.code(),
+ response.headers().toMultimap(),
+ body);
+ }
+
+ @Nullable
+ private String bodyAsString(Response response) {
+ if (response.body() == null) {
+ return null;
+ }
+
+ try {
+ return response.body().string();
+ } catch (IOException e) {
+ throw new ConductorClientException(response.message(),
+ e,
+ response.code(),
+ response.headers().toMultimap());
+ }
+ }
+
+ @SneakyThrows
+ private RequestBody serialize(String contentType, @NotNull Object body) {
+ //FIXME review this, what if we want to send something other than a JSON in the request
+ if (!isJsonMime(contentType)) {
+ throw new ConductorClientException("Content type \"" + contentType + "\" is not supported");
+ }
+
+ String content;
+ if (body instanceof String) {
+ content = (String) body;
+ } else {
+ content = objectMapper.writeValueAsString(body);
+ }
+
+ return RequestBody.create(content, MediaType.parse(contentType));
+ }
+
+ protected T handleResponse(Response response, Type returnType) {
+ if (!response.isSuccessful()) {
+ String respBody = bodyAsString(response);
+ throw new ConductorClientException(response.message(),
+ response.code(),
+ response.headers().toMultimap(),
+ respBody);
+ }
+
+ try {
+ if (returnType == null || response.code() == 204) {
+ return null;
+ } else {
+ return deserialize(response, returnType);
+ }
+ } finally {
+ if (response.body() != null) {
+ response.body().close();
+ }
+ }
+ }
+
+ protected Request buildRequest(String method,
+ String path,
+ List pathParams,
+ List queryParams,
+ Map headers,
+ Object body) {
+ final HttpUrl url = buildUrl(replacePathParams(path, pathParams), queryParams);
+ final Request.Builder requestBuilder = new Request.Builder().url(url);
+ processHeaderParams(requestBuilder, addHeadersFromProviders(method, path, headers));
+ RequestBody reqBody = requestBody(method, getContentType(headers), body);
+ return requestBuilder.method(method, reqBody).build();
+ }
+
+ private Map addHeadersFromProviders(String method, String path, Map headers) {
+ if (headerSuppliers.isEmpty()) {
+ return headers;
+ }
+
+ Map all = new HashMap<>();
+ for (HeaderSupplier supplier : headerSuppliers) {
+ all.putAll(supplier.get(method, path));
+ }
+ // request headers take precedence
+ all.putAll(headers);
+ return all;
+ }
+
+ @NotNull
+ private static String getContentType(Map headerParams) {
+ String contentType = headerParams.get("Content-Type");
+ if (contentType == null) {
+ contentType = "application/json";
+ }
+
+ return contentType;
+ }
+
+ private String replacePathParams(String path, List pathParams) {
+ for (Param param : pathParams) {
+ path = path.replace("{" + param.name() + "}", urlEncode(param.value()));
+ }
+
+ return path;
+ }
+
+ @Nullable
+ private RequestBody requestBody(String method, String contentType, Object body) {
+ if (!HttpMethod.permitsRequestBody(method)) {
+ return null;
+ }
+
+ if (body == null && "DELETE".equals(method)) {
+ return null;
+ } else if (body == null) {
+ return RequestBody.create("", MediaType.parse(contentType));
+ }
+
+ return serialize(contentType, body);
+ }
+
+ private HttpUrl buildUrl(String path, List queryParams) {
+ HttpUrl.Builder urlBuilder = Objects.requireNonNull(HttpUrl.parse(basePath + path))
+ .newBuilder();
+ for (Param param : queryParams) {
+ urlBuilder.addQueryParameter(param.name(), param.value());
+ }
+
+ return urlBuilder.build();
+ }
+
+ private void processHeaderParams(Request.Builder requestBuilder, Map headers) {
+ for (Entry header : headers.entrySet()) {
+ requestBuilder.header(header.getKey(), parameterToString(header.getValue()));
+ }
+ }
+
+ @SneakyThrows
+ private static void unsafeClient(OkHttpClient.Builder okhttpClientBuilder) {
+ LOGGER.warn("Unsafe client - Disabling SSL certificate validation is dangerous and should only be used in development environments");
+ // Create a trust manager that does not validate certificate chains
+ final TrustManager[] trustAllCerts = new TrustManager[]{
+ new X509TrustManager() {
+ @Override
+ public void checkClientTrusted(X509Certificate[] chain, String authType) {
+ }
+
+ @Override
+ public void checkServerTrusted(X509Certificate[] chain, String authType) {
+ }
+
+ @Override
+ public X509Certificate[] getAcceptedIssuers() {
+ return new X509Certificate[]{};
+ }
+ }
+ };
+ final SSLContext sslContext = SSLContext.getInstance("SSL");
+ sslContext.init(null, trustAllCerts, new SecureRandom());
+ // Creates a ssl socket factory with our all-trusting manager
+ final javax.net.ssl.SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
+ okhttpClientBuilder.sslSocketFactory(sslSocketFactory, (X509TrustManager) trustAllCerts[0]);
+ okhttpClientBuilder.hostnameVerifier((hostname, session) -> true);
+ }
+
+ //TODO review this - not sure if it's working 2024-08-07
+ private void trustCertificates(OkHttpClient.Builder okhttpClientBuilder) throws GeneralSecurityException {
+ CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
+ Collection extends Certificate> certificates = certificateFactory.generateCertificates(sslCaCert);
+ if (certificates.isEmpty()) {
+ throw new IllegalArgumentException("expected non-empty set of trusted certificates");
+ }
+ KeyStore caKeyStore = newEmptyKeyStore(null);
+ int index = 0;
+ for (Certificate certificate : certificates) {
+ String certificateAlias = "ca" + index++;
+ caKeyStore.setCertificateEntry(certificateAlias, certificate);
+ }
+
+ TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+ trustManagerFactory.init(caKeyStore);
+ TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
+ SSLContext sslContext = SSLContext.getInstance("TLS");
+ sslContext.init(keyManagers, trustManagers, new SecureRandom());
+ okhttpClientBuilder.sslSocketFactory(sslContext.getSocketFactory(), (X509TrustManager) trustManagers[0]);
+ }
+
+ private KeyStore newEmptyKeyStore(char[] password) throws GeneralSecurityException {
+ try {
+ KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
+ keyStore.load(null, password);
+ return keyStore;
+ } catch (IOException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ private ConductorClientResponse execute(Call call, Type returnType) {
+ try {
+ Response response = call.execute();
+ T data = handleResponse(response, returnType);
+ return new ConductorClientResponse<>(response.code(), response.headers().toMultimap(), data);
+ } catch (IOException e) {
+ throw new ConductorClientException(e);
+ }
+ }
+
+ public static class Builder {
+ private final OkHttpClient.Builder okHttpClientBuilder = new OkHttpClient.Builder();
+ private String basePath = "http://localhost:8080/api";
+ private boolean verifyingSsl = true;
+ private InputStream sslCaCert;
+ private KeyManager[] keyManagers;
+ private long connectTimeout = -1;
+ private long readTimeout = -1;
+ private long writeTimeout = -1;
+ private Proxy proxy;
+ private ConnectionPoolConfig connectionPoolConfig;
+ private Supplier objectMapperSupplier = () -> new ObjectMapperProvider().getObjectMapper();
+ private final List headerSuppliers = new ArrayList<>();
+
+ public String basePath() {
+ return basePath;
+ }
+
+ public Builder basePath(String basePath) {
+ this.basePath = basePath;
+ return this;
+ }
+
+ public boolean verifyingSsl() {
+ return verifyingSsl;
+ }
+
+ public Builder verifyingSsl(boolean verifyingSsl) {
+ this.verifyingSsl = verifyingSsl;
+ return this;
+ }
+
+ public InputStream sslCaCert() {
+ return sslCaCert;
+ }
+
+ public Builder sslCaCert(InputStream sslCaCert) {
+ this.sslCaCert = sslCaCert;
+ return this;
+ }
+
+ public KeyManager[] keyManagers() {
+ return keyManagers;
+ }
+
+ public Builder keyManagers(KeyManager[] keyManagers) {
+ this.keyManagers = keyManagers;
+ return this;
+ }
+
+ public long connectTimeout() {
+ return connectTimeout;
+ }
+
+ public Builder connectTimeout(long connectTimeout) {
+ this.connectTimeout = connectTimeout;
+ return this;
+ }
+
+ public long readTimeout() {
+ return readTimeout;
+ }
+
+ public Builder readTimeout(long readTimeout) {
+ this.readTimeout = readTimeout;
+ return this;
+ }
+
+ public long writeTimeout() {
+ return writeTimeout;
+ }
+
+ public Builder writeTimeout(long writeTimeout) {
+ this.writeTimeout = writeTimeout;
+ return this;
+ }
+
+ public Builder proxy(Proxy proxy) {
+ this.proxy = proxy;
+ return this;
+ }
+
+ public ConnectionPoolConfig getConnectionPoolConfig() {
+ return this.connectionPoolConfig;
+ }
+
+ public Builder connectionPoolConfig(ConnectionPoolConfig config) {
+ this.connectionPoolConfig = config;
+ return this;
+ }
+
+ Proxy getProxy() {
+ return proxy;
+ }
+
+ /**
+ * Use it to apply additional custom configurations to the OkHttp3 client. E.g.: add an interceptor.
+ *
+ * @param configurer
+ * @return
+ */
+ public Builder configureOkHttp(Consumer configurer) {
+ configurer.accept(this.okHttpClientBuilder);
+ return this;
+ }
+
+ /**
+ * Use it to supply a custom ObjectMapper.
+ *
+ * @param objectMapperSupplier
+ * @return
+ */
+ public Builder objectMapperSupplier(Supplier objectMapperSupplier) {
+ this.objectMapperSupplier = objectMapperSupplier;
+ return this;
+ }
+
+ public Builder addHeaderSupplier(HeaderSupplier headerSupplier) {
+ this.headerSuppliers.add(headerSupplier);
+ return this;
+ }
+
+ public List headerSupplier() {
+ return headerSuppliers;
+ }
+
+ public ConductorClient build() {
+ return new ConductorClient(this);
+ }
+
+ void validateAndAssignDefaults() {
+ if (StringUtils.isBlank(basePath)) {
+ throw new IllegalArgumentException("basePath cannot be blank");
+ }
+
+ if (basePath.endsWith("/")) {
+ basePath = basePath.substring(0, basePath.length() - 1);
+ }
+
+ }
+ }
+}
diff --git a/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/http/ConductorClientRequest.java b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/http/ConductorClientRequest.java
new file mode 100644
index 000000000..a1e0ffa12
--- /dev/null
+++ b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/http/ConductorClientRequest.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright 2024 Conductor 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 com.netflix.conductor.client.http;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+
+@Getter
+@EqualsAndHashCode
+public class ConductorClientRequest {
+
+ public enum Method {
+ GET, POST, PUT, DELETE, PATCH
+ }
+
+ private final Method method;
+ private final String path;
+ private final List pathParams;
+ private final List queryParams;
+ private final Map headerParams;
+ private final Object body;
+
+ private ConductorClientRequest(Builder builder) {
+ this.method = builder.method;
+ this.path = builder.path;
+ this.pathParams = builder.pathParams;
+ this.queryParams = builder.queryParams;
+ this.headerParams = builder.headerParams;
+ this.body = builder.body;
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static class Builder {
+ private Method method;
+ private String path;
+ private final List pathParams = new ArrayList<>();
+ private final List queryParams = new ArrayList<>();
+ private final Map headerParams = new HashMap<>();
+ private Object body;
+
+ public Builder method(Method method) {
+ if (method == null) {
+ throw new IllegalArgumentException("Method cannot be null");
+ }
+ this.method = method;
+ return this;
+ }
+
+ public Builder path(String path) {
+ if (path == null || path.isEmpty()) {
+ throw new IllegalArgumentException("Path cannot be null or empty");
+ }
+ this.path = path;
+ return this;
+ }
+
+ public Builder addPathParam(String name, Integer value) {
+ return addPathParam(name, Integer.toString(value));
+ }
+
+ public Builder addPathParam(String name, String value) {
+ if (name == null || name.isEmpty() || value == null) {
+ throw new IllegalArgumentException("Path parameter name and value cannot be null or empty");
+ }
+ this.pathParams.add(new Param(name, value));
+ return this;
+ }
+
+ public Builder addQueryParam(String name, Long value) {
+ if (value == null) {
+ return this;
+ }
+
+ addQueryParam(name, Long.toString(value));
+ return this;
+ }
+
+ public Builder addQueryParam(String name, Integer value) {
+ if (value == null) {
+ return this;
+ }
+
+ addQueryParam(name, Integer.toString(value));
+ return this;
+ }
+
+ public Builder addQueryParam(String name, Boolean value) {
+ if (value == null) {
+ return this;
+ }
+
+ addQueryParam(name, Boolean.toString(value));
+ return this;
+ }
+
+ public Builder addQueryParams(String name, List values) {
+ values.forEach(it -> addQueryParam(name, it));
+ return this;
+ }
+
+ public Builder addQueryParam(String name, String value) {
+ if (value == null) {
+ return this;
+ }
+
+ if (name == null || name.isEmpty()) {
+ throw new IllegalArgumentException("Query parameter name cannot be null or empty");
+ }
+
+ this.queryParams.add(new Param(name, value));
+ return this;
+ }
+
+ public Builder addHeaderParam(String name, String value) {
+ if (name == null || name.isEmpty() || value == null) {
+ throw new IllegalArgumentException("Header parameter name and value cannot be null or empty");
+ }
+
+ this.headerParams.put(name, value);
+ return this;
+ }
+
+ public Builder body(Object body) {
+ this.body = body;
+ return this;
+ }
+
+ public ConductorClientRequest build() {
+ return new ConductorClientRequest(this);
+ }
+ }
+}
diff --git a/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/http/ConductorClientResponse.java b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/http/ConductorClientResponse.java
new file mode 100644
index 000000000..537909eb4
--- /dev/null
+++ b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/http/ConductorClientResponse.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2022 Conductor 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 com.netflix.conductor.client.http;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * API response returned by API call.
+ *
+ * @param The type of data that is deserialized from response body
+ */
+public class ConductorClientResponse {
+ private final int statusCode;
+ private final Map> headers;
+ private final T data;
+
+ /**
+ * @param statusCode The status code of HTTP response
+ * @param headers The headers of HTTP response
+ */
+ public ConductorClientResponse(int statusCode, Map> headers) {
+ this(statusCode, headers, null);
+ }
+
+ /**
+ * @param statusCode The status code of HTTP response
+ * @param headers The headers of HTTP response
+ * @param data The object deserialized from response bod
+ */
+ public ConductorClientResponse(int statusCode, Map> headers, T data) {
+ this.statusCode = statusCode;
+ this.headers = headers;
+ this.data = data;
+ }
+
+ public int getStatusCode() {
+ return statusCode;
+ }
+
+ public Map> getHeaders() {
+ return headers;
+ }
+
+ public T getData() {
+ return data;
+ }
+}
diff --git a/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/http/ConnectionPoolConfig.java b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/http/ConnectionPoolConfig.java
new file mode 100644
index 000000000..5909a4197
--- /dev/null
+++ b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/http/ConnectionPoolConfig.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2024 Conductor 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 com.netflix.conductor.client.http;
+
+import java.util.concurrent.TimeUnit;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@AllArgsConstructor
+@Getter
+public class ConnectionPoolConfig {
+ private final int maxIdleConnections;
+ private final long keepAliveDuration;
+ private final TimeUnit timeUnit;
+}
diff --git a/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/http/EventClient.java b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/http/EventClient.java
new file mode 100644
index 000000000..ea536dc34
--- /dev/null
+++ b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/http/EventClient.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2022 Conductor 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 com.netflix.conductor.client.http;
+
+import java.util.List;
+
+import org.apache.commons.lang3.Validate;
+
+import com.netflix.conductor.client.http.ConductorClientRequest.Method;
+import com.netflix.conductor.common.metadata.events.EventHandler;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+
+
+
+// Client class for all Event Handler operations
+public final class EventClient {
+
+ private ConductorClient client;
+
+ /** Creates a default metadata client */
+ public EventClient() {
+ }
+
+ public EventClient(ConductorClient client) {
+ this.client = client;
+ }
+
+ /**
+ * Kept only for backwards compatibility
+ *
+ * @param rootUri basePath for the ApiClient
+ */
+ @Deprecated
+ public void setRootURI(String rootUri) {
+ if (client != null) {
+ client.shutdown();
+ }
+ client = new ConductorClient(rootUri);
+ }
+
+ /**
+ * Register an event handler with the server.
+ *
+ * @param eventHandler the eventHandler definition.
+ */
+ public void registerEventHandler(EventHandler eventHandler) {
+ Validate.notNull(eventHandler, "Event Handler definition cannot be null");
+ ConductorClientRequest request = ConductorClientRequest.builder()
+ .method(Method.POST)
+ .path("/event")
+ .body(eventHandler)
+ .build();
+
+ client.execute(request);
+ }
+
+ /**
+ * Updates an event handler with the server.
+ *
+ * @param eventHandler the eventHandler definition.
+ */
+ public void updateEventHandler(EventHandler eventHandler) {
+ Validate.notNull(eventHandler, "Event Handler definition cannot be null");
+ ConductorClientRequest request = ConductorClientRequest.builder()
+ .method(Method.PUT)
+ .path("/event")
+ .body(eventHandler)
+ .build();
+
+ client.execute(request);
+ }
+
+ /**
+ * @param event name of the event.
+ * @param activeOnly if true, returns only the active handlers.
+ * @return Returns the list of all the event handlers for a given event.
+ */
+ public List getEventHandlers(String event, boolean activeOnly) {
+ Validate.notBlank(event, "Event cannot be blank");
+
+ ConductorClientRequest request = ConductorClientRequest.builder()
+ .method(Method.GET)
+ .path("/event/{name}")
+ .addPathParam("name", event)
+ .addQueryParam("activeOnly", activeOnly)
+ .build();
+
+ ConductorClientResponse> resp = client.execute(request, new TypeReference<>() {
+ });
+
+ return resp.getData();
+ }
+
+ /**
+ * Removes the event handler definition from the conductor server
+ *
+ * @param name the name of the event handler to be unregistered
+ */
+ public void unregisterEventHandler(String name) {
+ Validate.notBlank(name, "Event handler name cannot be blank");
+ ConductorClientRequest request = ConductorClientRequest.builder()
+ .method(Method.DELETE)
+ .path("/event/{name}")
+ .addPathParam("name", name)
+ .build();
+ client.execute(request);
+ }
+}
diff --git a/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/http/HeaderSupplier.java b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/http/HeaderSupplier.java
new file mode 100644
index 000000000..e6b4cb377
--- /dev/null
+++ b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/http/HeaderSupplier.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2024 Conductor 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 com.netflix.conductor.client.http;
+
+import java.util.Map;
+
+
+public interface HeaderSupplier {
+
+ void init(ConductorClient client);
+
+ Map get(String method, String path);
+}
diff --git a/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/http/MetadataClient.java b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/http/MetadataClient.java
new file mode 100644
index 000000000..c9e88c978
--- /dev/null
+++ b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/http/MetadataClient.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright 2020 Conductor 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 com.netflix.conductor.client.http;
+
+import java.util.List;
+
+import org.apache.commons.lang3.Validate;
+
+import com.netflix.conductor.client.http.ConductorClientRequest.Method;
+import com.netflix.conductor.common.metadata.tasks.TaskDef;
+import com.netflix.conductor.common.metadata.workflow.WorkflowDef;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+
+
+public final class MetadataClient {
+
+ private ConductorClient client;
+
+ /** Creates a default metadata client */
+ public MetadataClient() {
+ }
+
+ public MetadataClient(ConductorClient client) {
+ this.client = client;
+ }
+
+ /**
+ * Kept only for backwards compatibility
+ *
+ * @param rootUri basePath for the ApiClient
+ */
+ @Deprecated
+ public void setRootURI(String rootUri) {
+ if (client != null) {
+ client.shutdown();
+ }
+ client = new ConductorClient(rootUri);
+ }
+
+ /**
+ * Register a workflow definition with the server
+ *
+ * @param workflowDef the workflow definition
+ */
+ public void registerWorkflowDef(WorkflowDef workflowDef) {
+ Validate.notNull(workflowDef, "WorkflowDef cannot be null");
+ ConductorClientRequest request = ConductorClientRequest.builder()
+ .method(Method.POST)
+ .path("/metadata/workflow")
+ .body(workflowDef)
+ .build();
+
+ client.execute(request);
+ }
+
+ /**
+ * Updates a list of existing workflow definitions
+ *
+ * @param workflowDefs List of workflow definitions to be updated
+ */
+ public void updateWorkflowDefs(List workflowDefs) {
+ Validate.notEmpty(workflowDefs, "Workflow definitions cannot be null or empty");
+ ConductorClientRequest request = ConductorClientRequest.builder()
+ .method(Method.PUT)
+ .path("/metadata/workflow")
+ .body(workflowDefs)
+ .build();
+
+ client.execute(request);
+ }
+
+ /**
+ * Retrieve the workflow definition
+ *
+ * @param name the name of the workflow
+ * @param version the version of the workflow def
+ * @return Workflow definition for the given workflow and version
+ */
+ public WorkflowDef getWorkflowDef(String name, Integer version) {
+ ConductorClientRequest request = ConductorClientRequest.builder()
+ .method(Method.GET)
+ .path("/metadata/workflow/{name}")
+ .addPathParam("name", name)
+ .addQueryParam("version", version)
+ .build();
+
+ ConductorClientResponse resp = client.execute(request, new TypeReference<>() {
+ });
+
+ return resp.getData();
+ }
+
+ public List getAllWorkflowsWithLatestVersions() {
+ ConductorClientRequest request = ConductorClientRequest.builder()
+ .method(Method.GET)
+ .path("metadata/workflow/latest-versions")
+ .build();
+
+ ConductorClientResponse> resp = client.execute(request, new TypeReference<>() {
+ });
+
+ return resp.getData();
+ }
+
+ /**
+ * Removes the workflow definition of a workflow from the conductor server. It does not remove
+ * associated workflows. Use with caution.
+ *
+ * @param name Name of the workflow to be unregistered.
+ * @param version Version of the workflow definition to be unregistered.
+ */
+ public void unregisterWorkflowDef(String name, Integer version) {
+ Validate.notBlank(name, "Name cannot be blank");
+ Validate.notNull(version, "version cannot be null");
+
+ ConductorClientRequest request = ConductorClientRequest.builder()
+ .method(Method.DELETE)
+ .path("/metadata/workflow/{name}/{version}")
+ .addPathParam("name", name)
+ .addPathParam("version", Integer.toString(version))
+ .build();
+
+ client.execute(request);
+ }
+
+ // Task Metadata Operations
+
+ /**
+ * Registers a list of task types with the conductor server
+ *
+ * @param taskDefs List of task types to be registered.
+ */
+ public void registerTaskDefs(List taskDefs) {
+ ConductorClientRequest request = ConductorClientRequest.builder()
+ .method(Method.POST)
+ .path("/metadata/taskdefs")
+ .body(taskDefs)
+ .build();
+
+ client.execute(request);
+ }
+
+ /**
+ * Updates an existing task definition
+ *
+ * @param taskDef the task definition to be updated
+ */
+ public void updateTaskDef(TaskDef taskDef) {
+ ConductorClientRequest request = ConductorClientRequest.builder()
+ .method(Method.PUT)
+ .path("/metadata/taskdefs")
+ .body(taskDef)
+ .build();
+
+ client.execute(request);
+ }
+
+ /**
+ * Retrieve the task definition of a given task type
+ *
+ * @param taskType type of task for which to retrieve the definition
+ * @return Task Definition for the given task type
+ */
+ public TaskDef getTaskDef(String taskType) {
+ ConductorClientRequest request = ConductorClientRequest.builder()
+ .method(Method.GET)
+ .path("/metadata/taskdefs/{taskType}")
+ .addPathParam("taskType", taskType)
+ .build();
+
+ ConductorClientResponse resp = client.execute(request, new TypeReference<>() {
+ });
+
+ return resp.getData();
+ }
+
+ /**
+ * Removes the task definition of a task type from the conductor server. Use with caution.
+ *
+ * @param taskType Task type to be unregistered.
+ */
+ public void unregisterTaskDef(String taskType) {
+ ConductorClientRequest request = ConductorClientRequest.builder()
+ .method(Method.DELETE)
+ .path("/metadata/taskdefs/{taskType}")
+ .addPathParam("taskType", taskType)
+ .build();
+
+ client.execute(request);
+ }
+
+ /**
+ *
+ * @return All the registered task definitions
+ */
+ public List getAllTaskDefs() {
+ ConductorClientRequest request = ConductorClientRequest.builder()
+ .method(Method.GET)
+ .path("/metadata/taskdefs")
+ .build();
+
+ ConductorClientResponse> resp = client.execute(request, new TypeReference<>() {
+ });
+
+ return resp.getData();
+ }
+}
diff --git a/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/http/Param.java b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/http/Param.java
new file mode 100644
index 000000000..6bc8ace51
--- /dev/null
+++ b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/http/Param.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2022 Conductor 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 com.netflix.conductor.client.http;
+
+import lombok.EqualsAndHashCode;
+import lombok.RequiredArgsConstructor;
+import lombok.ToString;
+
+/**
+ * Used for path variables and query params
+ */
+@RequiredArgsConstructor
+@EqualsAndHashCode
+@ToString
+public final class Param {
+ private final String name;
+ private final String value;
+
+ public String name() {
+ return name;
+ }
+
+ public String value() {
+ return value;
+ }
+}
diff --git a/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/http/TaskClient.java b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/http/TaskClient.java
new file mode 100644
index 000000000..279028a12
--- /dev/null
+++ b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/http/TaskClient.java
@@ -0,0 +1,459 @@
+/*
+ * Copyright 2022 Conductor 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 com.netflix.conductor.client.http;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.Validate;
+
+import com.netflix.conductor.client.http.ConductorClientRequest.Method;
+import com.netflix.conductor.common.metadata.tasks.PollData;
+import com.netflix.conductor.common.metadata.tasks.Task;
+import com.netflix.conductor.common.metadata.tasks.TaskExecLog;
+import com.netflix.conductor.common.metadata.tasks.TaskResult;
+import com.netflix.conductor.common.run.SearchResult;
+import com.netflix.conductor.common.run.TaskSummary;
+import com.netflix.conductor.common.utils.ExternalPayloadStorage;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+
+/** Client for conductor task management including polling for task, updating task status etc. */
+public final class TaskClient {
+
+ private ConductorClient client;
+
+ /** Creates a default task client */
+ public TaskClient() {
+ }
+
+ public TaskClient(ConductorClient client) {
+ this.client = client;
+ }
+
+ /**
+ * Kept only for backwards compatibility
+ *
+ * @param rootUri basePath for the ApiClient
+ */
+ @Deprecated
+ public void setRootURI(String rootUri) {
+ if (client != null) {
+ client.shutdown();
+ }
+ client = new ConductorClient(rootUri);
+ }
+
+ /**
+ * Perform a poll for a task of a specific task type.
+ *
+ * @param taskType The taskType to poll for
+ * @param domain The domain of the task type
+ * @param workerId Name of the client worker. Used for logging.
+ * @return Task waiting to be executed.
+ */
+ public Task pollTask(String taskType, String workerId, String domain){
+ Validate.notBlank(taskType, "Task type cannot be blank");
+ Validate.notBlank(workerId, "Worker id cannot be blank");
+
+ ConductorClientRequest request = ConductorClientRequest.builder()
+ .method(Method.GET)
+ .path("/tasks/poll/{taskType}")
+ .addPathParam("taskType", taskType)
+ .addQueryParam("workerid", workerId)
+ .addQueryParam("domain", domain)
+ .build();
+
+ ConductorClientResponse resp = client.execute(request, new TypeReference<>() {
+ });
+
+ Task task = resp.getData();
+ populateTaskPayloads(task);
+ return task;
+ }
+
+ /**
+ * Perform a batch poll for tasks by task type. Batch size is configurable by count.
+ *
+ * @param taskType Type of task to poll for
+ * @param workerId Name of the client worker. Used for logging.
+ * @param count Maximum number of tasks to be returned. Actual number of tasks returned can be
+ * less than this number.
+ * @param timeoutInMillisecond Long poll wait timeout.
+ * @return List of tasks awaiting to be executed.
+ */
+ public List batchPollTasksByTaskType(String taskType, String workerId, int count, int timeoutInMillisecond) {
+ Validate.notBlank(taskType, "Task type cannot be blank");
+ Validate.notBlank(workerId, "Worker id cannot be blank");
+ Validate.isTrue(count > 0, "Count must be greater than 0");
+
+ List tasks = batchPoll(taskType, workerId, null, count, timeoutInMillisecond);
+ tasks.forEach(this::populateTaskPayloads);
+ return tasks;
+ }
+
+ /**
+ * Batch poll for tasks in a domain. Batch size is configurable by count.
+ *
+ * @param taskType Type of task to poll for
+ * @param domain The domain of the task type
+ * @param workerId Name of the client worker. Used for logging.
+ * @param count Maximum number of tasks to be returned. Actual number of tasks returned can be
+ * less than this number.
+ * @param timeoutInMillisecond Long poll wait timeout.
+ * @return List of tasks awaiting to be executed.
+ */
+ public List batchPollTasksInDomain(String taskType, String domain, String workerId, int count, int timeoutInMillisecond){
+ Validate.notBlank(taskType, "Task type cannot be blank");
+ Validate.notBlank(workerId, "Worker id cannot be blank");
+ Validate.isTrue(count > 0, "Count must be greater than 0");
+
+ List tasks = batchPoll(taskType, workerId, domain, count, timeoutInMillisecond);
+ tasks.forEach(this::populateTaskPayloads);
+ return tasks;
+ }
+
+ /**
+ * Updates the result of a task execution. If the size of the task output payload is bigger than
+ * {@link ExternalPayloadStorage}, if enabled, else the task is marked as
+ * FAILED_WITH_TERMINAL_ERROR.
+ *
+ * @param taskResult the {@link TaskResult} of the executed task to be updated.
+ */
+ public void updateTask(TaskResult taskResult) {
+ Validate.notNull(taskResult, "Task result cannot be null");
+ ConductorClientRequest request = ConductorClientRequest.builder()
+ .method(Method.POST)
+ .path("/tasks")
+ .body(taskResult)
+ .build();
+
+ client.execute(request);
+ }
+
+ //TODO FIXME OSS MISMATCH - https://github.com/conductor-oss/conductor-java-sdk/issues/27
+ public Optional evaluateAndUploadLargePayload(Map taskOutputData, String taskType) {
+ throw new UnsupportedOperationException("No external storage support YET");
+ }
+
+ /**
+ * Ack for the task poll.
+ *
+ * @param taskId Id of the task to be polled
+ * @param workerId user identified worker.
+ * @return true if the task was found with the given ID and acknowledged. False otherwise. If
+ * the server returns false, the client should NOT attempt to ack again.
+ */
+ public Boolean ack(String taskId, String workerId) {
+ Validate.notBlank(taskId, "Task id cannot be blank");
+ ConductorClientRequest request = ConductorClientRequest.builder()
+ .method(Method.POST)
+ .path("tasks/{taskId}/ack")
+ .addPathParam("taskId", taskId)
+ .addQueryParam("workerid", workerId)
+ .build();
+
+ ConductorClientResponse response = client.execute(request, new TypeReference<>() {
+ });
+
+ return response.getData();
+ }
+
+ /**
+ * Log execution messages for a task.
+ *
+ * @param taskId id of the task
+ * @param logMessage the message to be logged
+ */
+ public void logMessageForTask(String taskId, String logMessage) {
+ Validate.notBlank(taskId, "Task id cannot be blank");
+ ConductorClientRequest request = ConductorClientRequest.builder()
+ .method(Method.POST)
+ .path("/tasks/{taskId}/log")
+ .addPathParam("taskId", taskId)
+ .body(logMessage)
+ .build();
+
+ client.execute(request);
+ }
+
+ /**
+ * Fetch execution logs for a task.
+ *
+ * @param taskId id of the task.
+ */
+ public List getTaskLogs(String taskId){
+ Validate.notBlank(taskId, "Task id cannot be blank");
+ ConductorClientRequest request = ConductorClientRequest.builder()
+ .method(Method.GET)
+ .path("/tasks/{taskId}/log")
+ .addPathParam("taskId", taskId)
+ .build();
+
+ ConductorClientResponse> resp = client.execute(request, new TypeReference<>() {
+ });
+
+ return resp.getData();
+ }
+
+ /**
+ * Retrieve information about the task
+ *
+ * @param taskId ID of the task
+ * @return Task details
+ */
+ public Task getTaskDetails(String taskId) {
+ Validate.notBlank(taskId, "Task id cannot be blank");
+ ConductorClientRequest request = ConductorClientRequest.builder()
+ .method(Method.GET)
+ .path("/tasks/{taskId}")
+ .addPathParam("taskId", taskId)
+ .build();
+
+ ConductorClientResponse resp = client.execute(request, new TypeReference<>() {
+ });
+
+ return resp.getData();
+ }
+
+ /**
+ * Removes a task from a taskType queue
+ *
+ * @param taskType the taskType to identify the queue
+ * @param taskId the id of the task to be removed
+ */
+ public void removeTaskFromQueue(String taskType, String taskId) {
+ Validate.notBlank(taskType, "Task type cannot be blank");
+ Validate.notBlank(taskId, "Task id cannot be blank");
+ ConductorClientRequest request = ConductorClientRequest.builder()
+ .method(Method.GET)
+ .path("tasks/queue/{taskType}/{taskId}")
+ .addPathParam("taskType", taskType)
+ .addPathParam("taskId", taskId)
+ .build();
+
+ client.execute(request);
+ }
+
+ public int getQueueSizeForTask(String taskType) {
+ return getQueueSizeForTask(taskType, null, null, null);
+ }
+
+ public int getQueueSizeForTask(String taskType, String domain, String isolationGroupId, String executionNamespace) {
+ Validate.notBlank(taskType, "Task type cannot be blank");
+ ConductorClientRequest request = ConductorClientRequest.builder()
+ .method(Method.GET)
+ .path("/tasks/queue/size") //FIXME Not supported by Orkes Conductor. Orkes Conductor only has "/tasks/queue/sizes"
+ .addQueryParam("taskType", taskType)
+ .addQueryParam("domain", domain)
+ .addQueryParam("isolationGroupId", isolationGroupId)
+ .addQueryParam("executionNamespace", executionNamespace)
+ .build();
+ ConductorClientResponse resp = client.execute(request, new TypeReference<>() {
+ });
+
+ Integer queueSize = resp.getData();
+ return queueSize != null ? queueSize : 0;
+ }
+
+ /**
+ * Get last poll data for a given task type
+ *
+ * @param taskType the task type for which poll data is to be fetched
+ * @return returns the list of poll data for the task type
+ */
+ public List getPollData(String taskType) {
+ Validate.notBlank(taskType, "Task type cannot be blank");
+ ConductorClientRequest request = ConductorClientRequest.builder()
+ .method(Method.GET)
+ .path("/tasks/queue/polldata")
+ .addQueryParam("taskType", taskType)
+ .build();
+ ConductorClientResponse> resp = client.execute(request, new TypeReference<>() {
+ });
+
+ return resp.getData();
+ }
+
+ /**
+ * Get the last poll data for all task types
+ *
+ * @return returns a list of poll data for all task types
+ */
+ public List getAllPollData() {
+ ConductorClientRequest request = ConductorClientRequest.builder()
+ .method(Method.GET)
+ .path("/tasks/queue/polldata")
+ .build();
+ ConductorClientResponse> resp = client.execute(request, new TypeReference<>() {
+ });
+
+ return resp.getData();
+ }
+
+ /**
+ * Requeue pending tasks for all running workflows
+ *
+ * @return returns the number of tasks that have been requeued
+ */
+ public String requeueAllPendingTasks() {
+ ConductorClientRequest request = ConductorClientRequest.builder()
+ .method(Method.POST)
+ .path("/tasks/queue/requeue")
+ .build();
+
+ ConductorClientResponse resp = client.execute(request, new TypeReference<>() {
+ });
+
+ return resp.getData();
+ }
+
+ /**
+ * Requeue pending tasks of a specific task type
+ *
+ * @return returns the number of tasks that have been requeued
+ */
+ public String requeuePendingTasksByTaskType(String taskType) {
+ Validate.notBlank(taskType, "Task type cannot be blank");
+ ConductorClientRequest request = ConductorClientRequest.builder()
+ .method(Method.POST)
+ .path("/tasks/queue/requeue/{taskType}")
+ .addPathParam("taskType", taskType)
+ .build();
+
+ ConductorClientResponse resp = client.execute(request, new TypeReference<>() {
+ });
+
+ return resp.getData();
+ }
+ /**
+ * Search for tasks based on payload
+ *
+ * @param query the search string
+ * @return returns the {@link SearchResult} containing the {@link TaskSummary} matching the
+ * query
+ */
+ public SearchResult search(String query) {
+ ConductorClientRequest request = ConductorClientRequest.builder()
+ .method(Method.GET)
+ .path("/tasks/search")
+ .addQueryParam("query", query)
+ .build();
+
+ ConductorClientResponse> resp = client.execute(request, new TypeReference<>() {
+ });
+
+ return resp.getData();
+ }
+
+ /**
+ * Search for tasks based on payload
+ *
+ * @param query the search string
+ * @return returns the {@link SearchResult} containing the {@link Task} matching the query
+ */
+ public SearchResult searchV2(String query) {
+ ConductorClientRequest request = ConductorClientRequest.builder()
+ .method(Method.GET)
+ .path("tasks/search-v2")
+ .addQueryParam("query", query)
+ .build();
+
+ ConductorClientResponse> resp = client.execute(request, new TypeReference<>() {
+ });
+
+ return resp.getData();
+ }
+
+ /**
+ * Paginated search for tasks based on payload
+ *
+ * @param start start value of page
+ * @param size number of tasks to be returned
+ * @param sort sort order
+ * @param freeText additional free text query
+ * @param query the search query
+ * @return the {@link SearchResult} containing the {@link TaskSummary} that match the query
+ */
+ public SearchResult search(Integer start, Integer size, String sort, String freeText, String query) {
+ ConductorClientRequest request = ConductorClientRequest.builder()
+ .method(Method.GET)
+ .path("/tasks/search")
+ .addQueryParam("start", start)
+ .addQueryParam("size", size)
+ .addQueryParam("sort", sort)
+ .addQueryParam("freeText", freeText)
+ .addQueryParam("query", query)
+ .build();
+
+ ConductorClientResponse> resp = client.execute(request, new TypeReference<>() {
+ });
+
+ return resp.getData();
+ }
+
+ /**
+ * Paginated search for tasks based on payload
+ *
+ * @param start start value of page
+ * @param size number of tasks to be returned
+ * @param sort sort order
+ * @param freeText additional free text query
+ * @param query the search query
+ * @return the {@link SearchResult} containing the {@link Task} that match the query
+ */
+ public SearchResult searchV2(Integer start, Integer size, String sort, String freeText, String query) {
+ ConductorClientRequest request = ConductorClientRequest.builder()
+ .method(Method.GET)
+ .path("tasks/search-v2")
+ .addQueryParam("start", start)
+ .addQueryParam("size", size)
+ .addQueryParam("sort", sort)
+ .addQueryParam("freeText", freeText)
+ .addQueryParam("query", query)
+ .build();
+
+ ConductorClientResponse> resp = client.execute(request, new TypeReference<>() {
+ });
+
+ return resp.getData();
+ }
+
+ //TODO FIXME OSS MISMATCH - https://github.com/conductor-oss/conductor-java-sdk/issues/27
+ //implement populateTaskPayloads - Download from external Storage and set input and output of task
+ private void populateTaskPayloads(Task task) {
+ if (StringUtils.isNotBlank(task.getExternalInputPayloadStoragePath())
+ || StringUtils.isNotBlank(task.getExternalOutputPayloadStoragePath())) {
+ throw new UnsupportedOperationException("No external storage support");
+ }
+ }
+
+ private List batchPoll(String taskType, String workerid, String domain, Integer count, Integer timeout) {
+ ConductorClientRequest request = ConductorClientRequest.builder()
+ .method(Method.GET)
+ .path("/tasks/poll/batch/{taskType}")
+ .addPathParam("taskType", taskType)
+ .addQueryParam("workerid", workerid)
+ .addQueryParam("domain", domain)
+ .addQueryParam("count", count)
+ .addQueryParam("timeout", timeout)
+ .build();
+
+ ConductorClientResponse> resp = client.execute(request, new TypeReference<>() {
+ });
+
+ return resp.getData();
+ }
+}
diff --git a/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/http/WorkflowClient.java b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/http/WorkflowClient.java
new file mode 100644
index 000000000..8330440d7
--- /dev/null
+++ b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/http/WorkflowClient.java
@@ -0,0 +1,497 @@
+/*
+ * Copyright 2021 Conductor 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 com.netflix.conductor.client.http;
+
+import java.util.List;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.Validate;
+
+import com.netflix.conductor.client.http.ConductorClientRequest.Method;
+import com.netflix.conductor.common.metadata.workflow.RerunWorkflowRequest;
+import com.netflix.conductor.common.metadata.workflow.SkipTaskRequest;
+import com.netflix.conductor.common.metadata.workflow.StartWorkflowRequest;
+import com.netflix.conductor.common.model.BulkResponse;
+import com.netflix.conductor.common.run.SearchResult;
+import com.netflix.conductor.common.run.Workflow;
+import com.netflix.conductor.common.run.WorkflowSummary;
+import com.netflix.conductor.common.run.WorkflowTestRequest;
+import com.netflix.conductor.common.utils.ExternalPayloadStorage;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+
+
+public final class WorkflowClient {
+
+ private ConductorClient client;
+
+ /** Creates a default workflow client */
+ public WorkflowClient() {
+ }
+
+ public WorkflowClient(ConductorClient client) {
+ this.client = client;
+ }
+
+ /**
+ * Kept only for backwards compatibility
+ *
+ * @param rootUri basePath for the ApiClient
+ */
+ @Deprecated
+ public void setRootURI(String rootUri) {
+ if (client != null) {
+ client.shutdown();
+ }
+ client = new ConductorClient(rootUri);
+ }
+
+ /**
+ * Starts a workflow. If the size of the workflow input payload is bigger than {@link
+ * ExternalPayloadStorage}, if enabled, else the workflow is rejected.
+ *
+ * @param startWorkflowRequest the {@link StartWorkflowRequest} object to start the workflow
+ * @return the id of the workflow instance that can be used for tracking
+ */
+ public String startWorkflow(StartWorkflowRequest startWorkflowRequest) {
+ Validate.notNull(startWorkflowRequest, "StartWorkflowRequest cannot be null");
+ Validate.notBlank(startWorkflowRequest.getName(), "Workflow name cannot be null or empty");
+ Validate.isTrue(
+ StringUtils.isBlank(startWorkflowRequest.getExternalInputPayloadStoragePath()),
+ "External Storage Path must not be set");
+
+ ConductorClientRequest request = ConductorClientRequest.builder()
+ .method(Method.POST)
+ .path("/workflow")
+ .body(startWorkflowRequest)
+ .build();
+
+ ConductorClientResponse resp = client.execute(request, new TypeReference<>() {
+ });
+
+ return resp.getData();
+ }
+
+ /**
+ * Retrieve a workflow by workflow id
+ *
+ * @param workflowId the id of the workflow
+ * @param includeTasks specify if the tasks in the workflow need to be returned
+ * @return the requested workflow
+ */
+ public Workflow getWorkflow(String workflowId, boolean includeTasks) {
+ Validate.notBlank(workflowId, "workflow id cannot be blank");
+ ConductorClientRequest request = ConductorClientRequest.builder()
+ .method(Method.GET)
+ .path("/workflow/{workflowId}")
+ .addPathParam("workflowId", workflowId)
+ .addQueryParam("includeTasks", includeTasks)
+ .build();
+
+ ConductorClientResponse resp = client.execute(request, new TypeReference<>() {
+ });
+
+ Workflow workflow = resp.getData();
+ populateWorkflowOutput(workflow);
+ return workflow;
+ }
+
+ /**
+ * Retrieve all workflows for a given correlation id and name
+ *
+ * @param name the name of the workflow
+ * @param correlationId the correlation id
+ * @param includeClosed specify if all workflows are to be returned or only running workflows
+ * @param includeTasks specify if the tasks in the workflow need to be returned
+ * @return list of workflows for the given correlation id and name
+ */
+ public List getWorkflows(String name, String correlationId, boolean includeClosed, boolean includeTasks){
+ Validate.notBlank(name, "name cannot be blank");
+ Validate.notBlank(correlationId, "correlationId cannot be blank");
+
+ ConductorClientRequest request = ConductorClientRequest.builder()
+ .method(Method.GET)
+ .path("/workflow/{name}/correlated/{correlationId}")
+ .addPathParam("name", name)
+ .addPathParam("correlationId", correlationId)
+ .addQueryParam("includeClosed", includeClosed)
+ .addQueryParam("includeTasks", includeTasks)
+ .build();
+
+ ConductorClientResponse> resp = client.execute(request, new TypeReference<>() {
+ });
+
+ List workflows = resp.getData();
+ workflows.forEach(this::populateWorkflowOutput);
+ return workflows;
+ }
+
+ /**
+ * Removes a workflow from the system
+ *
+ * @param workflowId the id of the workflow to be deleted
+ * @param archiveWorkflow flag to indicate if the workflow should be archived before deletion
+ */
+ public void deleteWorkflow(String workflowId, boolean archiveWorkflow) {
+ Validate.notBlank(workflowId, "Workflow id cannot be blank");
+ ConductorClientRequest request = ConductorClientRequest.builder()
+ .method(Method.DELETE)
+ .path("/workflow/{workflowId}/remove")
+ .addPathParam("workflowId", workflowId)
+ .addQueryParam("archiveWorkflow", archiveWorkflow)
+ .build();
+
+ client.execute(request);
+ }
+
+ /**
+ * Terminates the execution of all given workflows instances
+ *
+ * @param workflowIds the ids of the workflows to be terminated
+ * @param reason the reason to be logged and displayed
+ * @return the {@link BulkResponse} contains bulkErrorResults and bulkSuccessfulResults
+ */
+ public BulkResponse terminateWorkflows(List workflowIds, String reason) {
+ Validate.isTrue(!workflowIds.isEmpty(), "workflow id cannot be blank");
+
+ ConductorClientRequest request = ConductorClientRequest.builder()
+ .method(Method.POST)
+ .path("/workflow/bulk/terminate")
+ .addQueryParam("reason", reason)
+ .body(workflowIds)
+ .build();
+
+ ConductorClientResponse resp = client.execute(request, new TypeReference<>() {
+ });
+
+ return resp.getData();
+ }
+
+ /**
+ * Retrieve all running workflow instances for a given name and version
+ *
+ * @param workflowName the name of the workflow
+ * @param version the version of the wokflow definition. Defaults to 1.
+ * @return the list of running workflow instances
+ */
+ public List getRunningWorkflow(String workflowName, Integer version) {
+ return getRunningWorkflow(workflowName, version, null, null);
+ }
+
+ /**
+ * Retrieve all workflow instances for a given workflow name between a specific time period
+ *
+ * @param workflowName the name of the workflow
+ * @param version the version of the workflow definition. Defaults to 1.
+ * @param startTime the start time of the period
+ * @param endTime the end time of the period
+ * @return returns a list of workflows created during the specified during the time period
+ */
+ public List getWorkflowsByTimePeriod(String workflowName, int version, Long startTime, Long endTime) {
+ Validate.notBlank(workflowName, "Workflow name cannot be blank");
+ Validate.notNull(startTime, "Start time cannot be null");
+ Validate.notNull(endTime, "End time cannot be null");
+
+ return getRunningWorkflow(workflowName, version, startTime, endTime);
+ }
+
+ /**
+ * Starts the decision task for the given workflow instance
+ *
+ * @param workflowId the id of the workflow instance
+ */
+ public void runDecider(String workflowId) {
+ Validate.notBlank(workflowId, "workflow id cannot be blank");
+ ConductorClientRequest request = ConductorClientRequest.builder()
+ .method(Method.PUT)
+ .path("/workflow/decide/{workflowId}")
+ .addPathParam("workflowId", workflowId)
+ .build();
+
+ client.execute(request);
+ }
+
+ /**
+ * Pause a workflow by workflow id
+ *
+ * @param workflowId the workflow id of the workflow to be paused
+ */
+ public void pauseWorkflow(String workflowId) {
+ Validate.notBlank(workflowId, "workflow id cannot be blank");
+ ConductorClientRequest request = ConductorClientRequest.builder()
+ .method(Method.PUT)
+ .path("/workflow/{workflowId}/pause")
+ .addPathParam("workflowId", workflowId)
+ .build();
+
+ client.execute(request);
+ }
+
+ /**
+ * Resume a paused workflow by workflow id
+ *
+ * @param workflowId the workflow id of the paused workflow
+ */
+ public void resumeWorkflow(String workflowId) {
+ Validate.notBlank(workflowId, "workflow id cannot be blank");
+ ConductorClientRequest request = ConductorClientRequest.builder()
+ .method(Method.PUT)
+ .path("/workflow/{workflowId}/resume")
+ .addPathParam("workflowId", workflowId)
+ .build();
+
+ client.execute(request);
+ }
+
+ /**
+ * Skips a given task from a current RUNNING workflow
+ *
+ * @param workflowId the id of the workflow instance
+ * @param taskReferenceName the reference name of the task to be skipped
+ */
+ public void skipTaskFromWorkflow(String workflowId, String taskReferenceName) {
+ Validate.notBlank(workflowId, "workflow id cannot be blank");
+ Validate.notBlank(taskReferenceName, "Task reference name cannot be blank");
+
+ //FIXME skipTaskRequest content is always empty
+ SkipTaskRequest skipTaskRequest = new SkipTaskRequest();
+ ConductorClientRequest request = ConductorClientRequest.builder()
+ .method(Method.PUT)
+ .path("/workflow/{workflowId}/skiptask/{taskReferenceName}")
+ .addPathParam("workflowId", workflowId)
+ .addPathParam("taskReferenceName", taskReferenceName)
+ .body(skipTaskRequest) //FIXME review this. It was passed as a query param?!
+ .build();
+
+ client.execute(request);
+ }
+
+ /**
+ * Reruns the workflow from a specific task
+ *
+ * @param workflowId the id of the workflow
+ * @param rerunWorkflowRequest the request containing the task to rerun from
+ * @return the id of the workflow
+ */
+ public String rerunWorkflow(String workflowId, RerunWorkflowRequest rerunWorkflowRequest) {
+ Validate.notBlank(workflowId, "workflow id cannot be blank");
+ Validate.notNull(rerunWorkflowRequest, "RerunWorkflowRequest cannot be null");
+ ConductorClientRequest request = ConductorClientRequest.builder()
+ .method(Method.POST)
+ .path("/workflow/{workflowId}/rerun")
+ .addPathParam("workflowId", workflowId)
+ .body(rerunWorkflowRequest)
+ .build();
+
+ ConductorClientResponse resp = client.execute(request, new TypeReference<>() {
+ });
+
+ return resp.getData();
+ }
+
+ /**
+ * Restart a completed workflow
+ *
+ * @param workflowId the workflow id of the workflow to be restarted
+ * @param useLatestDefinitions if true, use the latest workflow and task definitions when
+ * restarting the workflow if false, use the workflow and task definitions embedded in the
+ * workflow execution when restarting the workflow
+ */
+ public void restart(String workflowId, boolean useLatestDefinitions) {
+ Validate.notBlank(workflowId, "workflow id cannot be blank");
+ ConductorClientRequest request = ConductorClientRequest.builder()
+ .method(Method.POST)
+ .path("/workflow/{workflowId}/restart")
+ .addPathParam("workflowId", workflowId)
+ .addQueryParam("useLatestDefinitions", useLatestDefinitions)
+ .build();
+
+ client.execute(request);
+ }
+
+ /**
+ * Retries the last failed task in a workflow
+ *
+ * @param workflowId the workflow id of the workflow with the failed task
+ */
+ public void retryLastFailedTask(String workflowId) {
+ Validate.notBlank(workflowId, "workflow id cannot be blank");
+ ConductorClientRequest request = ConductorClientRequest.builder()
+ .method(Method.POST)
+ .path("/workflow/{workflowId}/retry")
+ .addPathParam("workflowId", workflowId)
+ .build();
+
+ client.execute(request);
+ }
+
+ /**
+ * Resets the callback times of all IN PROGRESS tasks to 0 for the given workflow
+ *
+ * @param workflowId the id of the workflow
+ */
+ public void resetCallbacksForInProgressTasks(String workflowId) {
+ Validate.notBlank(workflowId, "workflow id cannot be blank");
+ ConductorClientRequest request = ConductorClientRequest.builder()
+ .method(Method.POST)
+ .path("/workflow/{workflowId}/resetcallbacks")
+ .addPathParam("workflowId", workflowId)
+ .build();
+ client.execute(request);
+ }
+
+ /**
+ * Terminates the execution of the given workflow instance
+ *
+ * @param workflowId the id of the workflow to be terminated
+ * @param reason the reason to be logged and displayed
+ */
+ public void terminateWorkflow(String workflowId, String reason) {
+ Validate.notBlank(workflowId, "workflow id cannot be blank");
+ ConductorClientRequest request = ConductorClientRequest.builder()
+ .method(Method.DELETE)
+ .path("/workflow/{workflowId}")
+ .addPathParam("workflowId", workflowId)
+ .addQueryParam("reason", reason)
+ .build();
+
+ client.execute(request);
+ }
+
+ /**
+ * Search for workflows based on payload
+ *
+ * @param query the search query
+ * @return the {@link SearchResult} containing the {@link WorkflowSummary} that match the query
+ */
+ public SearchResult search(String query) {
+ return search(null, null, null, "", query);
+ }
+
+ /**
+ * Search for workflows based on payload
+ *
+ * @param query the search query
+ * @return the {@link SearchResult} containing the {@link Workflow} that match the query
+ */
+ public SearchResult searchV2(String query) {
+ ConductorClientRequest request = ConductorClientRequest.builder()
+ .method(Method.GET)
+ .path("/workflow/search-v2")
+ .addQueryParam("query", query)
+ .build();
+
+ ConductorClientResponse> resp = client.execute(request, new TypeReference<>() {
+ });
+
+ return resp.getData();
+ }
+
+ /**
+ * Paginated search for workflows based on payload
+ *
+ * @param start start value of page
+ * @param size number of workflows to be returned
+ * @param sort sort order
+ * @param freeText additional free text query
+ * @param query the search query
+ * @return the {@link SearchResult} containing the {@link WorkflowSummary} that match the query
+ */
+ public SearchResult search(
+ Integer start, Integer size, String sort, String freeText, String query) {
+ ConductorClientRequest request = ConductorClientRequest.builder()
+ .method(Method.GET)
+ .path("/workflow/search")
+ .addQueryParam("start", start)
+ .addQueryParam("size", size)
+ .addQueryParam("sort", sort)
+ .addQueryParam("freeText", freeText)
+ .addQueryParam("query", query)
+ .build();
+
+ ConductorClientResponse> resp = client.execute(request, new TypeReference<>() {
+ });
+
+ return resp.getData();
+ }
+
+ /**
+ * Paginated search for workflows based on payload
+ *
+ * @param start start value of page
+ * @param size number of workflows to be returned
+ * @param sort sort order
+ * @param freeText additional free text query
+ * @param query the search query
+ * @return the {@link SearchResult} containing the {@link Workflow} that match the query
+ */
+ public SearchResult searchV2(Integer start, Integer size, String sort, String freeText, String query) {
+ ConductorClientRequest request = ConductorClientRequest.builder()
+ .method(Method.GET)
+ .path("/workflow/search-v2")
+ .addQueryParam("start", start)
+ .addQueryParam("size", size)
+ .addQueryParam("sort", sort)
+ .addQueryParam("freeText", freeText)
+ .addQueryParam("query", query)
+ .build();
+
+ ConductorClientResponse> resp = client.execute(request, new TypeReference<>() {
+ });
+
+ return resp.getData();
+ }
+
+ public Workflow testWorkflow(WorkflowTestRequest testRequest) {
+ ConductorClientRequest request = ConductorClientRequest.builder()
+ .method(Method.POST)
+ .path("/workflow/test")
+ .body(testRequest)
+ .build();
+
+ ConductorClientResponse resp = client.execute(request, new TypeReference<>() {
+ });
+
+ return resp.getData();
+ }
+
+
+ /**
+ * Populates the workflow output from external payload storage if the external storage path is
+ * specified.
+ *
+ * @param workflow the workflow for which the output is to be populated.
+ */
+ private void populateWorkflowOutput(Workflow workflow) {
+ //TODO FIXME OSS MISMATCH - https://github.com/conductor-oss/conductor-java-sdk/issues/27
+ if (StringUtils.isNotBlank(workflow.getExternalOutputPayloadStoragePath())) {
+ throw new UnsupportedOperationException("No external storage support");
+ }
+ }
+
+ private List getRunningWorkflow(String name, Integer version, Long startTime, Long endTime) {
+ ConductorClientRequest request = ConductorClientRequest.builder()
+ .method(Method.GET)
+ .path("/workflow/running/{name}")
+ .addPathParam("name", name)
+ .addQueryParam("version", version)
+ .addQueryParam("startTime", startTime)
+ .addQueryParam("endTime", endTime)
+ .build();
+
+ ConductorClientResponse> resp = client.execute(request, new TypeReference<>() {
+ });
+
+ return resp.getData();
+ }
+}
diff --git a/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/metrics/MetricsCollector.java b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/metrics/MetricsCollector.java
new file mode 100644
index 000000000..48275d8f7
--- /dev/null
+++ b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/metrics/MetricsCollector.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2024 Conductor 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 com.netflix.conductor.client.metrics;
+
+import com.netflix.conductor.client.automator.events.PollCompleted;
+import com.netflix.conductor.client.automator.events.PollFailure;
+import com.netflix.conductor.client.automator.events.PollStarted;
+import com.netflix.conductor.client.automator.events.TaskExecutionCompleted;
+import com.netflix.conductor.client.automator.events.TaskExecutionFailure;
+import com.netflix.conductor.client.automator.events.TaskExecutionStarted;
+
+public interface MetricsCollector {
+
+ void consume(PollFailure e);
+
+ void consume(PollCompleted e);
+
+ void consume(PollStarted e);
+
+ void consume(TaskExecutionStarted e);
+
+ void consume(TaskExecutionCompleted e);
+
+ void consume(TaskExecutionFailure e);
+}
diff --git a/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/worker/Worker.java b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/worker/Worker.java
new file mode 100644
index 000000000..0ea08be7e
--- /dev/null
+++ b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/client/worker/Worker.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2021 Conductor 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 com.netflix.conductor.client.worker;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.function.Function;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.netflix.conductor.client.config.PropertyFactory;
+import com.netflix.conductor.common.metadata.tasks.Task;
+import com.netflix.conductor.common.metadata.tasks.TaskResult;
+
+public interface Worker {
+
+ String PROP_DOMAIN = "domain";
+ String PROP_ALL_WORKERS = "all";
+ String PROP_LOG_INTERVAL = "log_interval";
+ String PROP_POLL_INTERVAL = "poll_interval";
+ String PROP_PAUSED = "paused";
+
+ /**
+ * Retrieve the name of the task definition the worker is currently working on.
+ *
+ * @return the name of the task definition.
+ */
+ String getTaskDefName();
+
+ /**
+ * Executes a task and returns the updated task.
+ *
+ * @param task Task to be executed.
+ * @return the {@link TaskResult} object If the task is not completed yet, return with the
+ * status as IN_PROGRESS.
+ */
+ TaskResult execute(Task task);
+
+ /**
+ * Called when the task coordinator fails to update the task to the server. Client should store
+ * the task id (in a database) and retry the update later
+ *
+ * @param task Task which cannot be updated back to the server.
+ */
+ default void onErrorUpdate(Task task) {}
+
+ /**
+ * Override this method to pause the worker from polling.
+ *
+ * @return true if the worker is paused and no more tasks should be polled from server.
+ */
+ default boolean paused() {
+ return PropertyFactory.getBoolean(getTaskDefName(), PROP_PAUSED, false);
+ }
+
+ /**
+ * Override this method to app specific rules.
+ *
+ * @return returns the serverId as the id of the instance that the worker is running.
+ */
+ default String getIdentity() {
+ String serverId;
+ try {
+ // What if 2 workers run in the same host?
+ serverId = InetAddress.getLocalHost().getHostName();
+ } catch (UnknownHostException e) {
+ serverId = System.getenv("HOSTNAME");
+ }
+
+ LoggerHolder.logger.debug("Setting worker id to {}", serverId);
+ return serverId;
+ }
+
+ /**
+ * Override this method to change the interval between polls.
+ *
+ * @return interval in millisecond at which the server should be polled for worker tasks.
+ */
+ default int getPollingInterval() {
+ return PropertyFactory.getInteger(getTaskDefName(), PROP_POLL_INTERVAL, 1000);
+ }
+
+ static Worker create(String taskType, Function executor) {
+ return new Worker() {
+
+ @Override
+ public String getTaskDefName() {
+ return taskType;
+ }
+
+ @Override
+ public TaskResult execute(Task task) {
+ return executor.apply(task);
+ }
+
+ @Override
+ public boolean paused() {
+ return Worker.super.paused();
+ }
+ };
+ }
+}
+
+final class LoggerHolder {
+ static final Logger logger = LoggerFactory.getLogger(Worker.class);
+}
diff --git a/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/common/config/ObjectMapperProvider.java b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/common/config/ObjectMapperProvider.java
new file mode 100644
index 000000000..04b36ecf2
--- /dev/null
+++ b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/common/config/ObjectMapperProvider.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2024 Conductor 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 com.netflix.conductor.common.config;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+
+public class ObjectMapperProvider {
+
+ private static final ObjectMapper objectMapper = _getObjectMapper();
+
+ public ObjectMapper getObjectMapper() {
+ return objectMapper;
+ }
+
+ private static ObjectMapper _getObjectMapper() {
+ final ObjectMapper objectMapper = new ObjectMapper();
+ objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+ objectMapper.configure(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES, false);
+ objectMapper.configure(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES, false);
+ objectMapper.setDefaultPropertyInclusion(
+ JsonInclude.Value.construct(
+ JsonInclude.Include.NON_NULL, JsonInclude.Include.ALWAYS));
+ objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
+ objectMapper.registerModule(new JavaTimeModule());
+ return objectMapper;
+ }
+}
\ No newline at end of file
diff --git a/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/common/metadata/Auditable.java b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/common/metadata/Auditable.java
new file mode 100644
index 000000000..bef2e1792
--- /dev/null
+++ b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/common/metadata/Auditable.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2020 Conductor 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 com.netflix.conductor.common.metadata;
+
+public abstract class Auditable {
+
+ private String ownerApp;
+
+ private Long createTime;
+
+ private Long updateTime;
+
+ private String createdBy;
+
+ private String updatedBy;
+
+ /**
+ * @return the ownerApp
+ */
+ public String getOwnerApp() {
+ return ownerApp;
+ }
+
+ /**
+ * @param ownerApp the ownerApp to set
+ */
+ public void setOwnerApp(String ownerApp) {
+ this.ownerApp = ownerApp;
+ }
+
+ /**
+ * @return the createTime
+ */
+ public Long getCreateTime() {
+ return createTime == null ? 0 : createTime;
+ }
+
+ /**
+ * @param createTime the createTime to set
+ */
+ public void setCreateTime(Long createTime) {
+ this.createTime = createTime;
+ }
+
+ /**
+ * @return the updateTime
+ */
+ public Long getUpdateTime() {
+ return updateTime == null ? 0 : updateTime;
+ }
+
+ /**
+ * @param updateTime the updateTime to set
+ */
+ public void setUpdateTime(Long updateTime) {
+ this.updateTime = updateTime;
+ }
+
+ /**
+ * @return the createdBy
+ */
+ public String getCreatedBy() {
+ return createdBy;
+ }
+
+ /**
+ * @param createdBy the createdBy to set
+ */
+ public void setCreatedBy(String createdBy) {
+ this.createdBy = createdBy;
+ }
+
+ /**
+ * @return the updatedBy
+ */
+ public String getUpdatedBy() {
+ return updatedBy;
+ }
+
+ /**
+ * @param updatedBy the updatedBy to set
+ */
+ public void setUpdatedBy(String updatedBy) {
+ this.updatedBy = updatedBy;
+ }
+}
diff --git a/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/common/metadata/SchemaDef.java b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/common/metadata/SchemaDef.java
new file mode 100644
index 000000000..e61cd40d8
--- /dev/null
+++ b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/common/metadata/SchemaDef.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2024 Conductor 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 com.netflix.conductor.common.metadata;
+
+import java.util.Map;
+
+public class SchemaDef extends Auditable {
+
+ public enum Type {
+
+ JSON, AVRO, PROTOBUF
+ }
+
+ private String name;
+
+ private final int version = 1;
+
+ private Type type;
+
+ // Schema definition stored here
+ private Map data;
+
+ // Externalized schema definition (eg. via AVRO, Protobuf registry)
+ // If using Orkes Schema registry, this points to the name of the schema in the registry
+ private String externalRef;
+}
diff --git a/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/common/metadata/events/EventExecution.java b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/common/metadata/events/EventExecution.java
new file mode 100644
index 000000000..211c98913
--- /dev/null
+++ b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/common/metadata/events/EventExecution.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright 2020 Conductor 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 com.netflix.conductor.common.metadata.events;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+import com.netflix.conductor.common.metadata.events.EventHandler.Action;
+
+public class EventExecution {
+
+ public enum Status {
+
+ IN_PROGRESS, COMPLETED, FAILED, SKIPPED
+ }
+
+ private String id;
+
+ private String messageId;
+
+ private String name;
+
+ private String event;
+
+ private long created;
+
+ private Status status;
+
+ private Action.Type action;
+
+ private Map output = new HashMap<>();
+
+ public EventExecution() {
+ }
+
+ public EventExecution(String id, String messageId) {
+ this.id = id;
+ this.messageId = messageId;
+ }
+
+ /**
+ * @return the id
+ */
+ public String getId() {
+ return id;
+ }
+
+ /**
+ * @param id the id to set
+ */
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ /**
+ * @return the messageId
+ */
+ public String getMessageId() {
+ return messageId;
+ }
+
+ /**
+ * @param messageId the messageId to set
+ */
+ public void setMessageId(String messageId) {
+ this.messageId = messageId;
+ }
+
+ /**
+ * @return the name
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * @param name the name to set
+ */
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ /**
+ * @return the event
+ */
+ public String getEvent() {
+ return event;
+ }
+
+ /**
+ * @param event the event to set
+ */
+ public void setEvent(String event) {
+ this.event = event;
+ }
+
+ /**
+ * @return the created
+ */
+ public long getCreated() {
+ return created;
+ }
+
+ /**
+ * @param created the created to set
+ */
+ public void setCreated(long created) {
+ this.created = created;
+ }
+
+ /**
+ * @return the status
+ */
+ public Status getStatus() {
+ return status;
+ }
+
+ /**
+ * @param status the status to set
+ */
+ public void setStatus(Status status) {
+ this.status = status;
+ }
+
+ /**
+ * @return the action
+ */
+ public Action.Type getAction() {
+ return action;
+ }
+
+ /**
+ * @param action the action to set
+ */
+ public void setAction(Action.Type action) {
+ this.action = action;
+ }
+
+ /**
+ * @return the output
+ */
+ public Map getOutput() {
+ return output;
+ }
+
+ /**
+ * @param output the output to set
+ */
+ public void setOutput(Map output) {
+ this.output = output;
+ }
+
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ EventExecution execution = (EventExecution) o;
+ return created == execution.created && Objects.equals(id, execution.id) && Objects.equals(messageId, execution.messageId) && Objects.equals(name, execution.name) && Objects.equals(event, execution.event) && status == execution.status && action == execution.action && Objects.equals(output, execution.output);
+ }
+
+ public int hashCode() {
+ return Objects.hash(id, messageId, name, event, created, status, action, output);
+ }
+}
diff --git a/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/common/metadata/events/EventHandler.java b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/common/metadata/events/EventHandler.java
new file mode 100644
index 000000000..4887993bc
--- /dev/null
+++ b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/common/metadata/events/EventHandler.java
@@ -0,0 +1,474 @@
+/*
+ * Copyright 2020 Conductor 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 com.netflix.conductor.common.metadata.events;
+
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Defines an event handler
+ */
+public class EventHandler {
+
+ private String name;
+
+ private String event;
+
+ private String condition;
+
+ private List actions = new LinkedList<>();
+
+ private boolean active;
+
+ private String evaluatorType;
+
+ public EventHandler() {
+ }
+
+ /**
+ * @return the name MUST be unique within a conductor instance
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * @param name the name to set
+ */
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ /**
+ * @return the event
+ */
+ public String getEvent() {
+ return event;
+ }
+
+ /**
+ * @param event the event to set
+ */
+ public void setEvent(String event) {
+ this.event = event;
+ }
+
+ /**
+ * @return the condition
+ */
+ public String getCondition() {
+ return condition;
+ }
+
+ /**
+ * @param condition the condition to set
+ */
+ public void setCondition(String condition) {
+ this.condition = condition;
+ }
+
+ /**
+ * @return the actions
+ */
+ public List getActions() {
+ return actions;
+ }
+
+ /**
+ * @param actions the actions to set
+ */
+ public void setActions(List actions) {
+ this.actions = actions;
+ }
+
+ /**
+ * @return the active
+ */
+ public boolean isActive() {
+ return active;
+ }
+
+ /**
+ * @param active if set to false, the event handler is deactivated
+ */
+ public void setActive(boolean active) {
+ this.active = active;
+ }
+
+ /**
+ * @return the evaluator type
+ */
+ public String getEvaluatorType() {
+ return evaluatorType;
+ }
+
+ /**
+ * @param evaluatorType the evaluatorType to set
+ */
+ public void setEvaluatorType(String evaluatorType) {
+ this.evaluatorType = evaluatorType;
+ }
+
+ public static class Action {
+
+ public enum Type {
+
+ start_workflow, complete_task, fail_task, terminate_workflow, update_workflow_variables
+ }
+
+ private Type action;
+
+ private StartWorkflow start_workflow;
+
+ private TaskDetails complete_task;
+
+ private TaskDetails fail_task;
+
+ private boolean expandInlineJSON;
+
+ private TerminateWorkflow terminate_workflow;
+
+ private UpdateWorkflowVariables update_workflow_variables;
+
+ /**
+ * @return the action
+ */
+ public Type getAction() {
+ return action;
+ }
+
+ /**
+ * @param action the action to set
+ */
+ public void setAction(Type action) {
+ this.action = action;
+ }
+
+ /**
+ * @return the start_workflow
+ */
+ public StartWorkflow getStart_workflow() {
+ return start_workflow;
+ }
+
+ /**
+ * @param start_workflow the start_workflow to set
+ */
+ public void setStart_workflow(StartWorkflow start_workflow) {
+ this.start_workflow = start_workflow;
+ }
+
+ /**
+ * @return the complete_task
+ */
+ public TaskDetails getComplete_task() {
+ return complete_task;
+ }
+
+ /**
+ * @param complete_task the complete_task to set
+ */
+ public void setComplete_task(TaskDetails complete_task) {
+ this.complete_task = complete_task;
+ }
+
+ /**
+ * @return the fail_task
+ */
+ public TaskDetails getFail_task() {
+ return fail_task;
+ }
+
+ /**
+ * @param fail_task the fail_task to set
+ */
+ public void setFail_task(TaskDetails fail_task) {
+ this.fail_task = fail_task;
+ }
+
+ /**
+ * @param expandInlineJSON when set to true, the in-lined JSON strings are expanded to a
+ * full json document
+ */
+ public void setExpandInlineJSON(boolean expandInlineJSON) {
+ this.expandInlineJSON = expandInlineJSON;
+ }
+
+ /**
+ * @return true if the json strings within the payload should be expanded.
+ */
+ public boolean isExpandInlineJSON() {
+ return expandInlineJSON;
+ }
+
+ /**
+ * @return the terminate_workflow
+ */
+ public TerminateWorkflow getTerminate_workflow() {
+ return terminate_workflow;
+ }
+
+ /**
+ * @param terminate_workflow the terminate_workflow to set
+ */
+ public void setTerminate_workflow(TerminateWorkflow terminate_workflow) {
+ this.terminate_workflow = terminate_workflow;
+ }
+
+ /**
+ * @return the update_workflow_variables
+ */
+ public UpdateWorkflowVariables getUpdate_workflow_variables() {
+ return update_workflow_variables;
+ }
+
+ /**
+ * @param update_workflow_variables the update_workflow_variables to set
+ */
+ public void setUpdate_workflow_variables(UpdateWorkflowVariables update_workflow_variables) {
+ this.update_workflow_variables = update_workflow_variables;
+ }
+ }
+
+ public static class TaskDetails {
+
+ private String workflowId;
+
+ private String taskRefName;
+
+ private Map output = new HashMap<>();
+
+ private String taskId;
+
+ /**
+ * @return the workflowId
+ */
+ public String getWorkflowId() {
+ return workflowId;
+ }
+
+ /**
+ * @param workflowId the workflowId to set
+ */
+ public void setWorkflowId(String workflowId) {
+ this.workflowId = workflowId;
+ }
+
+ /**
+ * @return the taskRefName
+ */
+ public String getTaskRefName() {
+ return taskRefName;
+ }
+
+ /**
+ * @param taskRefName the taskRefName to set
+ */
+ public void setTaskRefName(String taskRefName) {
+ this.taskRefName = taskRefName;
+ }
+
+ /**
+ * @return the output
+ */
+ public Map getOutput() {
+ return output;
+ }
+
+ /**
+ * @param output the output to set
+ */
+ public void setOutput(Map output) {
+ this.output = output;
+ }
+
+ /**
+ * @return the taskId
+ */
+ public String getTaskId() {
+ return taskId;
+ }
+
+ /**
+ * @param taskId the taskId to set
+ */
+ public void setTaskId(String taskId) {
+ this.taskId = taskId;
+ }
+ }
+
+ public static class StartWorkflow {
+
+ private String name;
+
+ private Integer version;
+
+ private String correlationId;
+
+ private Map input = new HashMap<>();
+
+ private Map taskToDomain;
+
+ /**
+ * @return the name
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * @param name the name to set
+ */
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ /**
+ * @return the version
+ */
+ public Integer getVersion() {
+ return version;
+ }
+
+ /**
+ * @param version the version to set
+ */
+ public void setVersion(Integer version) {
+ this.version = version;
+ }
+
+ /**
+ * @return the correlationId
+ */
+ public String getCorrelationId() {
+ return correlationId;
+ }
+
+ /**
+ * @param correlationId the correlationId to set
+ */
+ public void setCorrelationId(String correlationId) {
+ this.correlationId = correlationId;
+ }
+
+ /**
+ * @return the input
+ */
+ public Map getInput() {
+ return input;
+ }
+
+ /**
+ * @param input the input to set
+ */
+ public void setInput(Map input) {
+ this.input = input;
+ }
+
+ public Map getTaskToDomain() {
+ return taskToDomain;
+ }
+
+ public void setTaskToDomain(Map taskToDomain) {
+ this.taskToDomain = taskToDomain;
+ }
+ }
+
+ public static class TerminateWorkflow {
+
+ private String workflowId;
+
+ private String terminationReason;
+
+ /**
+ * @return the workflowId
+ */
+ public String getWorkflowId() {
+ return workflowId;
+ }
+
+ /**
+ * @param workflowId the workflowId to set
+ */
+ public void setWorkflowId(String workflowId) {
+ this.workflowId = workflowId;
+ }
+
+ /**
+ * @return the reasonForTermination
+ */
+ public String getTerminationReason() {
+ return terminationReason;
+ }
+
+ /**
+ * @param terminationReason the reasonForTermination to set
+ */
+ public void setTerminationReason(String terminationReason) {
+ this.terminationReason = terminationReason;
+ }
+ }
+
+ public static class UpdateWorkflowVariables {
+
+ private String workflowId;
+
+ private Map variables;
+
+ private Boolean appendArray;
+
+ /**
+ * @return the workflowId
+ */
+ public String getWorkflowId() {
+ return workflowId;
+ }
+
+ /**
+ * @param workflowId the workflowId to set
+ */
+ public void setWorkflowId(String workflowId) {
+ this.workflowId = workflowId;
+ }
+
+ /**
+ * @return the variables
+ */
+ public Map getVariables() {
+ return variables;
+ }
+
+ /**
+ * @param variables the variables to set
+ */
+ public void setVariables(Map variables) {
+ this.variables = variables;
+ }
+
+ /**
+ * @return appendArray
+ */
+ public Boolean isAppendArray() {
+ return appendArray;
+ }
+
+ /**
+ * @param appendArray the appendArray to set
+ */
+ public void setAppendArray(Boolean appendArray) {
+ this.appendArray = appendArray;
+ }
+ }
+}
diff --git a/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/common/metadata/tasks/PollData.java b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/common/metadata/tasks/PollData.java
new file mode 100644
index 000000000..5801922aa
--- /dev/null
+++ b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/common/metadata/tasks/PollData.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2020 Conductor 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 com.netflix.conductor.common.metadata.tasks;
+
+import java.util.Objects;
+
+public class PollData {
+
+ private String queueName;
+
+ private String domain;
+
+ private String workerId;
+
+ private long lastPollTime;
+
+ public PollData() {
+ super();
+ }
+
+ public PollData(String queueName, String domain, String workerId, long lastPollTime) {
+ super();
+ this.queueName = queueName;
+ this.domain = domain;
+ this.workerId = workerId;
+ this.lastPollTime = lastPollTime;
+ }
+
+ public String getQueueName() {
+ return queueName;
+ }
+
+ public void setQueueName(String queueName) {
+ this.queueName = queueName;
+ }
+
+ public String getDomain() {
+ return domain;
+ }
+
+ public void setDomain(String domain) {
+ this.domain = domain;
+ }
+
+ public String getWorkerId() {
+ return workerId;
+ }
+
+ public void setWorkerId(String workerId) {
+ this.workerId = workerId;
+ }
+
+ public long getLastPollTime() {
+ return lastPollTime;
+ }
+
+ public void setLastPollTime(long lastPollTime) {
+ this.lastPollTime = lastPollTime;
+ }
+
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ PollData pollData = (PollData) o;
+ return getLastPollTime() == pollData.getLastPollTime() && Objects.equals(getQueueName(), pollData.getQueueName()) && Objects.equals(getDomain(), pollData.getDomain()) && Objects.equals(getWorkerId(), pollData.getWorkerId());
+ }
+
+ public int hashCode() {
+ return Objects.hash(getQueueName(), getDomain(), getWorkerId(), getLastPollTime());
+ }
+
+ public String toString() {
+ return "PollData{" + "queueName='" + queueName + '\'' + ", domain='" + domain + '\'' + ", workerId='" + workerId + '\'' + ", lastPollTime=" + lastPollTime + '}';
+ }
+}
diff --git a/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/common/metadata/tasks/Task.java b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/common/metadata/tasks/Task.java
new file mode 100644
index 000000000..d36239227
--- /dev/null
+++ b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/common/metadata/tasks/Task.java
@@ -0,0 +1,775 @@
+/*
+ * Copyright 2022 Conductor 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 com.netflix.conductor.common.metadata.tasks;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+
+import org.apache.commons.lang3.StringUtils;
+
+import com.netflix.conductor.common.metadata.workflow.WorkflowTask;
+
+public class Task {
+
+ public enum Status {
+
+ IN_PROGRESS(false, true, true),
+ CANCELED(true, false, false),
+ FAILED(true, false, true),
+ FAILED_WITH_TERMINAL_ERROR(true, false, // No retries even if retries are configured, the task and the related
+ false),
+ // workflow should be terminated
+ COMPLETED(true, true, true),
+ COMPLETED_WITH_ERRORS(true, true, true),
+ SCHEDULED(false, true, true),
+ TIMED_OUT(true, false, true),
+ SKIPPED(true, true, false);
+
+ private final boolean terminal;
+
+ private final boolean successful;
+
+ private final boolean retriable;
+
+ Status(boolean terminal, boolean successful, boolean retriable) {
+ this.terminal = terminal;
+ this.successful = successful;
+ this.retriable = retriable;
+ }
+
+ public boolean isTerminal() {
+ return terminal;
+ }
+
+ public boolean isSuccessful() {
+ return successful;
+ }
+
+ public boolean isRetriable() {
+ return retriable;
+ }
+ }
+
+ private String taskType;
+
+ private Status status;
+
+ private Map inputData = new HashMap<>();
+
+ private String referenceTaskName;
+
+ private int retryCount;
+
+ private int seq;
+
+ private String correlationId;
+
+ private int pollCount;
+
+ private String taskDefName;
+
+ /**
+ * Time when the task was scheduled
+ */
+ private long scheduledTime;
+
+ /**
+ * Time when the task was first polled
+ */
+ private long startTime;
+
+ /**
+ * Time when the task completed executing
+ */
+ private long endTime;
+
+ /**
+ * Time when the task was last updated
+ */
+ private long updateTime;
+
+ private int startDelayInSeconds;
+
+ private String retriedTaskId;
+
+ private boolean retried;
+
+ private boolean executed;
+
+ private boolean callbackFromWorker = true;
+
+ private long responseTimeoutSeconds;
+
+ private String workflowInstanceId;
+
+ private String workflowType;
+
+ private String taskId;
+
+ private String reasonForIncompletion;
+
+ private long callbackAfterSeconds;
+
+ private String workerId;
+
+ private Map outputData = new HashMap<>();
+
+ private WorkflowTask workflowTask;
+
+ private String domain;
+
+ // id 31 is reserved
+ private int rateLimitPerFrequency;
+
+ private int rateLimitFrequencyInSeconds;
+
+ private String externalInputPayloadStoragePath;
+
+ private String externalOutputPayloadStoragePath;
+
+ private int workflowPriority;
+
+ private String executionNameSpace;
+
+ private String isolationGroupId;
+
+ private int iteration;
+
+ private String subWorkflowId;
+
+ /**
+ * Use to note that a sub workflow associated with SUB_WORKFLOW task has an action performed on
+ * it directly.
+ */
+ private boolean subworkflowChanged;
+
+ // If the task is an event associated with a parent task, the id of the parent task
+ private String parentTaskId;
+
+ public Task() {
+ }
+
+ /**
+ * @return Type of the task
+ * @see TaskType
+ */
+ public String getTaskType() {
+ return taskType;
+ }
+
+ public void setTaskType(String taskType) {
+ this.taskType = taskType;
+ }
+
+ /**
+ * @return Status of the task
+ */
+ public Status getStatus() {
+ return status;
+ }
+
+ /**
+ * @param status Status of the task
+ */
+ public void setStatus(Status status) {
+ this.status = status;
+ }
+
+ public Map getInputData() {
+ return inputData;
+ }
+
+ public void setInputData(Map inputData) {
+ if (inputData == null) {
+ inputData = new HashMap<>();
+ }
+ this.inputData = inputData;
+ }
+
+ /**
+ * @return the referenceTaskName
+ */
+ public String getReferenceTaskName() {
+ return referenceTaskName;
+ }
+
+ /**
+ * @param referenceTaskName the referenceTaskName to set
+ */
+ public void setReferenceTaskName(String referenceTaskName) {
+ this.referenceTaskName = referenceTaskName;
+ }
+
+ /**
+ * @return the correlationId
+ */
+ public String getCorrelationId() {
+ return correlationId;
+ }
+
+ /**
+ * @param correlationId the correlationId to set
+ */
+ public void setCorrelationId(String correlationId) {
+ this.correlationId = correlationId;
+ }
+
+ /**
+ * @return the retryCount
+ */
+ public int getRetryCount() {
+ return retryCount;
+ }
+
+ /**
+ * @param retryCount the retryCount to set
+ */
+ public void setRetryCount(int retryCount) {
+ this.retryCount = retryCount;
+ }
+
+ /**
+ * @return the scheduledTime
+ */
+ public long getScheduledTime() {
+ return scheduledTime;
+ }
+
+ /**
+ * @param scheduledTime the scheduledTime to set
+ */
+ public void setScheduledTime(long scheduledTime) {
+ this.scheduledTime = scheduledTime;
+ }
+
+ /**
+ * @return the startTime
+ */
+ public long getStartTime() {
+ return startTime;
+ }
+
+ /**
+ * @param startTime the startTime to set
+ */
+ public void setStartTime(long startTime) {
+ this.startTime = startTime;
+ }
+
+ /**
+ * @return the endTime
+ */
+ public long getEndTime() {
+ return endTime;
+ }
+
+ /**
+ * @param endTime the endTime to set
+ */
+ public void setEndTime(long endTime) {
+ this.endTime = endTime;
+ }
+
+ /**
+ * @return the startDelayInSeconds
+ */
+ public int getStartDelayInSeconds() {
+ return startDelayInSeconds;
+ }
+
+ /**
+ * @param startDelayInSeconds the startDelayInSeconds to set
+ */
+ public void setStartDelayInSeconds(int startDelayInSeconds) {
+ this.startDelayInSeconds = startDelayInSeconds;
+ }
+
+ /**
+ * @return the retriedTaskId
+ */
+ public String getRetriedTaskId() {
+ return retriedTaskId;
+ }
+
+ /**
+ * @param retriedTaskId the retriedTaskId to set
+ */
+ public void setRetriedTaskId(String retriedTaskId) {
+ this.retriedTaskId = retriedTaskId;
+ }
+
+ /**
+ * @return the seq
+ */
+ public int getSeq() {
+ return seq;
+ }
+
+ /**
+ * @param seq the seq to set
+ */
+ public void setSeq(int seq) {
+ this.seq = seq;
+ }
+
+ /**
+ * @return the updateTime
+ */
+ public long getUpdateTime() {
+ return updateTime;
+ }
+
+ /**
+ * @param updateTime the updateTime to set
+ */
+ public void setUpdateTime(long updateTime) {
+ this.updateTime = updateTime;
+ }
+
+ /**
+ * @return the queueWaitTime
+ */
+ public long getQueueWaitTime() {
+ if (this.startTime > 0 && this.scheduledTime > 0) {
+ if (this.updateTime > 0 && getCallbackAfterSeconds() > 0) {
+ long waitTime = System.currentTimeMillis() - (this.updateTime + (getCallbackAfterSeconds() * 1000));
+ return waitTime > 0 ? waitTime : 0;
+ } else {
+ return this.startTime - this.scheduledTime;
+ }
+ }
+ return 0L;
+ }
+
+ /**
+ * @return True if the task has been retried after failure
+ */
+ public boolean isRetried() {
+ return retried;
+ }
+
+ /**
+ * @param retried the retried to set
+ */
+ public void setRetried(boolean retried) {
+ this.retried = retried;
+ }
+
+ /**
+ * @return True if the task has completed its lifecycle within conductor (from start to
+ * completion to being updated in the datastore)
+ */
+ public boolean isExecuted() {
+ return executed;
+ }
+
+ /**
+ * @param executed the executed value to set
+ */
+ public void setExecuted(boolean executed) {
+ this.executed = executed;
+ }
+
+ /**
+ * @return No. of times task has been polled
+ */
+ public int getPollCount() {
+ return pollCount;
+ }
+
+ public void setPollCount(int pollCount) {
+ this.pollCount = pollCount;
+ }
+
+ public void incrementPollCount() {
+ ++this.pollCount;
+ }
+
+ public boolean isCallbackFromWorker() {
+ return callbackFromWorker;
+ }
+
+ public void setCallbackFromWorker(boolean callbackFromWorker) {
+ this.callbackFromWorker = callbackFromWorker;
+ }
+
+ /**
+ * @return Name of the task definition
+ */
+ public String getTaskDefName() {
+ if (taskDefName == null || "".equals(taskDefName)) {
+ taskDefName = taskType;
+ }
+ return taskDefName;
+ }
+
+ /**
+ * @param taskDefName Name of the task definition
+ */
+ public void setTaskDefName(String taskDefName) {
+ this.taskDefName = taskDefName;
+ }
+
+ /**
+ * @return the timeout for task to send response. After this timeout, the task will be re-queued
+ */
+ public long getResponseTimeoutSeconds() {
+ return responseTimeoutSeconds;
+ }
+
+ /**
+ * @param responseTimeoutSeconds - timeout for task to send response. After this timeout, the
+ * task will be re-queued
+ */
+ public void setResponseTimeoutSeconds(long responseTimeoutSeconds) {
+ this.responseTimeoutSeconds = responseTimeoutSeconds;
+ }
+
+ /**
+ * @return the workflowInstanceId
+ */
+ public String getWorkflowInstanceId() {
+ return workflowInstanceId;
+ }
+
+ /**
+ * @param workflowInstanceId the workflowInstanceId to set
+ */
+ public void setWorkflowInstanceId(String workflowInstanceId) {
+ this.workflowInstanceId = workflowInstanceId;
+ }
+
+ public String getWorkflowType() {
+ return workflowType;
+ }
+
+ /**
+ * @param workflowType the name of the workflow
+ * @return the task object with the workflow type set
+ */
+ public com.netflix.conductor.common.metadata.tasks.Task setWorkflowType(String workflowType) {
+ this.workflowType = workflowType;
+ return this;
+ }
+
+ /**
+ * @return the taskId
+ */
+ public String getTaskId() {
+ return taskId;
+ }
+
+ /**
+ * @param taskId the taskId to set
+ */
+ public void setTaskId(String taskId) {
+ this.taskId = taskId;
+ }
+
+ /**
+ * @return the reasonForIncompletion
+ */
+ public String getReasonForIncompletion() {
+ return reasonForIncompletion;
+ }
+
+ /**
+ * @param reasonForIncompletion the reasonForIncompletion to set
+ */
+ public void setReasonForIncompletion(String reasonForIncompletion) {
+ this.reasonForIncompletion = StringUtils.substring(reasonForIncompletion, 0, 500);
+ }
+
+ /**
+ * @return the callbackAfterSeconds
+ */
+ public long getCallbackAfterSeconds() {
+ return callbackAfterSeconds;
+ }
+
+ /**
+ * @param callbackAfterSeconds the callbackAfterSeconds to set
+ */
+ public void setCallbackAfterSeconds(long callbackAfterSeconds) {
+ this.callbackAfterSeconds = callbackAfterSeconds;
+ }
+
+ /**
+ * @return the workerId
+ */
+ public String getWorkerId() {
+ return workerId;
+ }
+
+ /**
+ * @param workerId the workerId to set
+ */
+ public void setWorkerId(String workerId) {
+ this.workerId = workerId;
+ }
+
+ /**
+ * @return the outputData
+ */
+ public Map getOutputData() {
+ return outputData;
+ }
+
+ /**
+ * @param outputData the outputData to set
+ */
+ public void setOutputData(Map outputData) {
+ if (outputData == null) {
+ outputData = new HashMap<>();
+ }
+ this.outputData = outputData;
+ }
+
+ /**
+ * @return Workflow Task definition
+ */
+ public WorkflowTask getWorkflowTask() {
+ return workflowTask;
+ }
+
+ /**
+ * @param workflowTask Task definition
+ */
+ public void setWorkflowTask(WorkflowTask workflowTask) {
+ this.workflowTask = workflowTask;
+ }
+
+ /**
+ * @return the domain
+ */
+ public String getDomain() {
+ return domain;
+ }
+
+ /**
+ * @param domain the Domain
+ */
+ public void setDomain(String domain) {
+ this.domain = domain;
+ }
+
+ /**
+ * @return {@link Optional} containing the task definition if available
+ */
+ public Optional getTaskDefinition() {
+ return Optional.ofNullable(this.getWorkflowTask()).map(WorkflowTask::getTaskDefinition);
+ }
+
+ public int getRateLimitPerFrequency() {
+ return rateLimitPerFrequency;
+ }
+
+ public void setRateLimitPerFrequency(int rateLimitPerFrequency) {
+ this.rateLimitPerFrequency = rateLimitPerFrequency;
+ }
+
+ public int getRateLimitFrequencyInSeconds() {
+ return rateLimitFrequencyInSeconds;
+ }
+
+ public void setRateLimitFrequencyInSeconds(int rateLimitFrequencyInSeconds) {
+ this.rateLimitFrequencyInSeconds = rateLimitFrequencyInSeconds;
+ }
+
+ /**
+ * @return the external storage path for the task input payload
+ */
+ public String getExternalInputPayloadStoragePath() {
+ return externalInputPayloadStoragePath;
+ }
+
+ /**
+ * @param externalInputPayloadStoragePath the external storage path where the task input payload
+ * is stored
+ */
+ public void setExternalInputPayloadStoragePath(String externalInputPayloadStoragePath) {
+ this.externalInputPayloadStoragePath = externalInputPayloadStoragePath;
+ }
+
+ /**
+ * @return the external storage path for the task output payload
+ */
+ public String getExternalOutputPayloadStoragePath() {
+ return externalOutputPayloadStoragePath;
+ }
+
+ /**
+ * @param externalOutputPayloadStoragePath the external storage path where the task output
+ * payload is stored
+ */
+ public void setExternalOutputPayloadStoragePath(String externalOutputPayloadStoragePath) {
+ this.externalOutputPayloadStoragePath = externalOutputPayloadStoragePath;
+ }
+
+ public void setIsolationGroupId(String isolationGroupId) {
+ this.isolationGroupId = isolationGroupId;
+ }
+
+ public String getIsolationGroupId() {
+ return isolationGroupId;
+ }
+
+ public String getExecutionNameSpace() {
+ return executionNameSpace;
+ }
+
+ public void setExecutionNameSpace(String executionNameSpace) {
+ this.executionNameSpace = executionNameSpace;
+ }
+
+ /**
+ * @return the iteration
+ */
+ public int getIteration() {
+ return iteration;
+ }
+
+ /**
+ * @param iteration iteration
+ */
+ public void setIteration(int iteration) {
+ this.iteration = iteration;
+ }
+
+ public boolean isLoopOverTask() {
+ return iteration > 0;
+ }
+
+ /**
+ * @return the priority defined on workflow
+ */
+ public int getWorkflowPriority() {
+ return workflowPriority;
+ }
+
+ /**
+ * @param workflowPriority Priority defined for workflow
+ */
+ public void setWorkflowPriority(int workflowPriority) {
+ this.workflowPriority = workflowPriority;
+ }
+
+ public boolean isSubworkflowChanged() {
+ return subworkflowChanged;
+ }
+
+ public void setSubworkflowChanged(boolean subworkflowChanged) {
+ this.subworkflowChanged = subworkflowChanged;
+ }
+
+ public String getSubWorkflowId() {
+ // For backwards compatibility
+ if (StringUtils.isNotBlank(subWorkflowId)) {
+ return subWorkflowId;
+ } else {
+ return this.getOutputData() != null && this.getOutputData().get("subWorkflowId") != null ? (String) this.getOutputData().get("subWorkflowId") : this.getInputData() != null ? (String) this.getInputData().get("subWorkflowId") : null;
+ }
+ }
+
+ public void setSubWorkflowId(String subWorkflowId) {
+ this.subWorkflowId = subWorkflowId;
+ // For backwards compatibility
+ if (this.getOutputData() != null && this.getOutputData().containsKey("subWorkflowId")) {
+ this.getOutputData().put("subWorkflowId", subWorkflowId);
+ }
+ }
+
+ public String getParentTaskId() {
+ return parentTaskId;
+ }
+
+ public void setParentTaskId(String parentTaskId) {
+ this.parentTaskId = parentTaskId;
+ }
+
+ public Task copy() {
+ Task copy = new Task();
+ copy.setCallbackAfterSeconds(callbackAfterSeconds);
+ copy.setCallbackFromWorker(callbackFromWorker);
+ copy.setCorrelationId(correlationId);
+ copy.setInputData(inputData);
+ copy.setOutputData(outputData);
+ copy.setReferenceTaskName(referenceTaskName);
+ copy.setStartDelayInSeconds(startDelayInSeconds);
+ copy.setTaskDefName(taskDefName);
+ copy.setTaskType(taskType);
+ copy.setWorkflowInstanceId(workflowInstanceId);
+ copy.setWorkflowType(workflowType);
+ copy.setResponseTimeoutSeconds(responseTimeoutSeconds);
+ copy.setStatus(status);
+ copy.setRetryCount(retryCount);
+ copy.setPollCount(pollCount);
+ copy.setTaskId(taskId);
+ copy.setWorkflowTask(workflowTask);
+ copy.setDomain(domain);
+ copy.setRateLimitPerFrequency(rateLimitPerFrequency);
+ copy.setRateLimitFrequencyInSeconds(rateLimitFrequencyInSeconds);
+ copy.setExternalInputPayloadStoragePath(externalInputPayloadStoragePath);
+ copy.setExternalOutputPayloadStoragePath(externalOutputPayloadStoragePath);
+ copy.setWorkflowPriority(workflowPriority);
+ copy.setIteration(iteration);
+ copy.setExecutionNameSpace(executionNameSpace);
+ copy.setIsolationGroupId(isolationGroupId);
+ copy.setSubWorkflowId(getSubWorkflowId());
+ copy.setSubworkflowChanged(subworkflowChanged);
+ copy.setParentTaskId(parentTaskId);
+ return copy;
+ }
+
+ /**
+ * @return a deep copy of the task instance To be used inside copy Workflow method to provide a
+ * valid deep copied object. Note: This does not copy the following fields:
+ *
+ * - retried
+ *
- updateTime
+ *
- retriedTaskId
+ *
+ */
+ public Task deepCopy() {
+ Task deepCopy = copy();
+ deepCopy.setStartTime(startTime);
+ deepCopy.setScheduledTime(scheduledTime);
+ deepCopy.setEndTime(endTime);
+ deepCopy.setWorkerId(workerId);
+ deepCopy.setReasonForIncompletion(reasonForIncompletion);
+ deepCopy.setSeq(seq);
+ deepCopy.setParentTaskId(parentTaskId);
+ return deepCopy;
+ }
+
+ public String toString() {
+ return "Task{" + "taskType='" + taskType + '\'' + ", status=" + status + ", inputData=" + inputData + ", referenceTaskName='" + referenceTaskName + '\'' + ", retryCount=" + retryCount + ", seq=" + seq + ", correlationId='" + correlationId + '\'' + ", pollCount=" + pollCount + ", taskDefName='" + taskDefName + '\'' + ", scheduledTime=" + scheduledTime + ", startTime=" + startTime + ", endTime=" + endTime + ", updateTime=" + updateTime + ", startDelayInSeconds=" + startDelayInSeconds + ", retriedTaskId='" + retriedTaskId + '\'' + ", retried=" + retried + ", executed=" + executed + ", callbackFromWorker=" + callbackFromWorker + ", responseTimeoutSeconds=" + responseTimeoutSeconds + ", workflowInstanceId='" + workflowInstanceId + '\'' + ", workflowType='" + workflowType + '\'' + ", taskId='" + taskId + '\'' + ", reasonForIncompletion='" + reasonForIncompletion + '\'' + ", callbackAfterSeconds=" + callbackAfterSeconds + ", workerId='" + workerId + '\'' + ", outputData=" + outputData + ", workflowTask=" + workflowTask + ", domain='" + domain + '\'' + ", rateLimitPerFrequency=" + rateLimitPerFrequency + ", rateLimitFrequencyInSeconds=" + rateLimitFrequencyInSeconds + ", workflowPriority=" + workflowPriority + ", externalInputPayloadStoragePath='" + externalInputPayloadStoragePath + '\'' + ", externalOutputPayloadStoragePath='" + externalOutputPayloadStoragePath + '\'' + ", isolationGroupId='" + isolationGroupId + '\'' + ", executionNameSpace='" + executionNameSpace + '\'' + ", subworkflowChanged='" + subworkflowChanged + '\'' + '}';
+ }
+
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ Task task = (Task) o;
+ return getRetryCount() == task.getRetryCount() && getSeq() == task.getSeq() && getPollCount() == task.getPollCount() && getScheduledTime() == task.getScheduledTime() && getStartTime() == task.getStartTime() && getEndTime() == task.getEndTime() && getUpdateTime() == task.getUpdateTime() && getStartDelayInSeconds() == task.getStartDelayInSeconds() && isRetried() == task.isRetried() && isExecuted() == task.isExecuted() && isCallbackFromWorker() == task.isCallbackFromWorker() && getResponseTimeoutSeconds() == task.getResponseTimeoutSeconds() && getCallbackAfterSeconds() == task.getCallbackAfterSeconds() && getRateLimitPerFrequency() == task.getRateLimitPerFrequency() && getRateLimitFrequencyInSeconds() == task.getRateLimitFrequencyInSeconds() && Objects.equals(getTaskType(), task.getTaskType()) && getStatus() == task.getStatus() && getIteration() == task.getIteration() && getWorkflowPriority() == task.getWorkflowPriority() && Objects.equals(getInputData(), task.getInputData()) && Objects.equals(getReferenceTaskName(), task.getReferenceTaskName()) && Objects.equals(getCorrelationId(), task.getCorrelationId()) && Objects.equals(getTaskDefName(), task.getTaskDefName()) && Objects.equals(getRetriedTaskId(), task.getRetriedTaskId()) && Objects.equals(getWorkflowInstanceId(), task.getWorkflowInstanceId()) && Objects.equals(getWorkflowType(), task.getWorkflowType()) && Objects.equals(getTaskId(), task.getTaskId()) && Objects.equals(getReasonForIncompletion(), task.getReasonForIncompletion()) && Objects.equals(getWorkerId(), task.getWorkerId()) && Objects.equals(getOutputData(), task.getOutputData()) && Objects.equals(getWorkflowTask(), task.getWorkflowTask()) && Objects.equals(getDomain(), task.getDomain()) && Objects.equals(getExternalInputPayloadStoragePath(), task.getExternalInputPayloadStoragePath()) && Objects.equals(getExternalOutputPayloadStoragePath(), task.getExternalOutputPayloadStoragePath()) && Objects.equals(getIsolationGroupId(), task.getIsolationGroupId()) && Objects.equals(getExecutionNameSpace(), task.getExecutionNameSpace()) && Objects.equals(getParentTaskId(), task.getParentTaskId());
+ }
+
+ public int hashCode() {
+ return Objects.hash(getTaskType(), getStatus(), getInputData(), getReferenceTaskName(), getWorkflowPriority(), getRetryCount(), getSeq(), getCorrelationId(), getPollCount(), getTaskDefName(), getScheduledTime(), getStartTime(), getEndTime(), getUpdateTime(), getStartDelayInSeconds(), getRetriedTaskId(), isRetried(), isExecuted(), isCallbackFromWorker(), getResponseTimeoutSeconds(), getWorkflowInstanceId(), getWorkflowType(), getTaskId(), getReasonForIncompletion(), getCallbackAfterSeconds(), getWorkerId(), getOutputData(), getWorkflowTask(), getDomain(), getRateLimitPerFrequency(), getRateLimitFrequencyInSeconds(), getExternalInputPayloadStoragePath(), getExternalOutputPayloadStoragePath(), getIsolationGroupId(), getExecutionNameSpace(), getParentTaskId());
+ }
+}
diff --git a/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/common/metadata/tasks/TaskDef.java b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/common/metadata/tasks/TaskDef.java
new file mode 100644
index 000000000..d4bfc0fc1
--- /dev/null
+++ b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/common/metadata/tasks/TaskDef.java
@@ -0,0 +1,437 @@
+/*
+ * Copyright 2021 Conductor 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 com.netflix.conductor.common.metadata.tasks;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+import com.netflix.conductor.common.metadata.Auditable;
+import com.netflix.conductor.common.metadata.SchemaDef;
+
+public class TaskDef extends Auditable {
+
+ public enum TimeoutPolicy {
+
+ RETRY, TIME_OUT_WF, ALERT_ONLY
+ }
+
+ public enum RetryLogic {
+
+ FIXED, EXPONENTIAL_BACKOFF, LINEAR_BACKOFF
+ }
+
+ public static final int ONE_HOUR = 60 * 60;
+
+ /**
+ * Unique name identifying the task. The name is unique across
+ */
+ private String name;
+
+ private String description;
+
+ private int // Default
+ retryCount = 3;
+
+ private long timeoutSeconds;
+
+ private List inputKeys = new ArrayList<>();
+
+ private List outputKeys = new ArrayList<>();
+
+ private TimeoutPolicy timeoutPolicy = TimeoutPolicy.TIME_OUT_WF;
+
+ private RetryLogic retryLogic = RetryLogic.FIXED;
+
+ private int retryDelaySeconds = 60;
+
+ private long responseTimeoutSeconds = ONE_HOUR;
+
+ private Integer concurrentExecLimit;
+
+ private Map inputTemplate = new HashMap<>();
+
+ // This field is deprecated, do not use id 13.
+ // @ProtoField(id = 13)
+ // private Integer rateLimitPerSecond;
+ private Integer rateLimitPerFrequency;
+
+ private Integer rateLimitFrequencyInSeconds;
+
+ private String isolationGroupId;
+
+ private String executionNameSpace;
+
+ private String ownerEmail;
+
+ private Integer pollTimeoutSeconds;
+
+ private Integer backoffScaleFactor = 1;
+
+ private String baseType;
+
+ private SchemaDef inputSchema;
+
+ private SchemaDef outputSchema;
+
+ private boolean enforceSchema;
+
+ public TaskDef() {
+ }
+
+ public TaskDef(String name) {
+ this.name = name;
+ }
+
+ public TaskDef(String name, String description) {
+ this.name = name;
+ this.description = description;
+ }
+
+ public TaskDef(String name, String description, int retryCount, long timeoutSeconds) {
+ this.name = name;
+ this.description = description;
+ this.retryCount = retryCount;
+ this.timeoutSeconds = timeoutSeconds;
+ }
+
+ public TaskDef(String name, String description, String ownerEmail, int retryCount, long timeoutSeconds, long responseTimeoutSeconds) {
+ this.name = name;
+ this.description = description;
+ this.ownerEmail = ownerEmail;
+ this.retryCount = retryCount;
+ this.timeoutSeconds = timeoutSeconds;
+ this.responseTimeoutSeconds = responseTimeoutSeconds;
+ }
+
+ /**
+ * @return the name
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * @param name the name to set
+ */
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ /**
+ * @return the description
+ */
+ public String getDescription() {
+ return description;
+ }
+
+ /**
+ * @param description the description to set
+ */
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ /**
+ * @return the retryCount
+ */
+ public int getRetryCount() {
+ return retryCount;
+ }
+
+ /**
+ * @param retryCount the retryCount to set
+ */
+ public void setRetryCount(int retryCount) {
+ this.retryCount = retryCount;
+ }
+
+ /**
+ * @return the timeoutSeconds
+ */
+ public long getTimeoutSeconds() {
+ return timeoutSeconds;
+ }
+
+ /**
+ * @param timeoutSeconds the timeoutSeconds to set
+ */
+ public void setTimeoutSeconds(long timeoutSeconds) {
+ this.timeoutSeconds = timeoutSeconds;
+ }
+
+ /**
+ * @return Returns the input keys
+ */
+ public List getInputKeys() {
+ return inputKeys;
+ }
+
+ /**
+ * @param inputKeys Set of keys that the task accepts in the input map
+ */
+ public void setInputKeys(List inputKeys) {
+ this.inputKeys = inputKeys;
+ }
+
+ /**
+ * @return Returns the output keys for the task when executed
+ */
+ public List getOutputKeys() {
+ return outputKeys;
+ }
+
+ /**
+ * @param outputKeys Sets the output keys
+ */
+ public void setOutputKeys(List outputKeys) {
+ this.outputKeys = outputKeys;
+ }
+
+ /**
+ * @return the timeoutPolicy
+ */
+ public TimeoutPolicy getTimeoutPolicy() {
+ return timeoutPolicy;
+ }
+
+ /**
+ * @param timeoutPolicy the timeoutPolicy to set
+ */
+ public void setTimeoutPolicy(TimeoutPolicy timeoutPolicy) {
+ this.timeoutPolicy = timeoutPolicy;
+ }
+
+ /**
+ * @return the retryLogic
+ */
+ public RetryLogic getRetryLogic() {
+ return retryLogic;
+ }
+
+ /**
+ * @param retryLogic the retryLogic to set
+ */
+ public void setRetryLogic(RetryLogic retryLogic) {
+ this.retryLogic = retryLogic;
+ }
+
+ /**
+ * @return the retryDelaySeconds
+ */
+ public int getRetryDelaySeconds() {
+ return retryDelaySeconds;
+ }
+
+ /**
+ * @return the timeout for task to send response. After this timeout, the task will be re-queued
+ */
+ public long getResponseTimeoutSeconds() {
+ return responseTimeoutSeconds;
+ }
+
+ /**
+ * @param responseTimeoutSeconds - timeout for task to send response. After this timeout, the
+ * task will be re-queued
+ */
+ public void setResponseTimeoutSeconds(long responseTimeoutSeconds) {
+ this.responseTimeoutSeconds = responseTimeoutSeconds;
+ }
+
+ /**
+ * @param retryDelaySeconds the retryDelaySeconds to set
+ */
+ public void setRetryDelaySeconds(int retryDelaySeconds) {
+ this.retryDelaySeconds = retryDelaySeconds;
+ }
+
+ /**
+ * @return the inputTemplate
+ */
+ public Map getInputTemplate() {
+ return inputTemplate;
+ }
+
+ /**
+ * @return rateLimitPerFrequency The max number of tasks that will be allowed to be executed per
+ * rateLimitFrequencyInSeconds.
+ */
+ public Integer getRateLimitPerFrequency() {
+ return rateLimitPerFrequency == null ? 0 : rateLimitPerFrequency;
+ }
+
+ /**
+ * @param rateLimitPerFrequency The max number of tasks that will be allowed to be executed per
+ * rateLimitFrequencyInSeconds. Setting the value to 0 removes the rate limit
+ */
+ public void setRateLimitPerFrequency(Integer rateLimitPerFrequency) {
+ this.rateLimitPerFrequency = rateLimitPerFrequency;
+ }
+
+ /**
+ * @return rateLimitFrequencyInSeconds: The time bucket that is used to rate limit tasks based
+ * on {@link #getRateLimitPerFrequency()} If null or not set, then defaults to 1 second
+ */
+ public Integer getRateLimitFrequencyInSeconds() {
+ return rateLimitFrequencyInSeconds == null ? 1 : rateLimitFrequencyInSeconds;
+ }
+
+ /**
+ * @param rateLimitFrequencyInSeconds: The time window/bucket for which the rate limit needs to
+ * be applied. This will only have affect if {@link #getRateLimitPerFrequency()} is greater
+ * than zero
+ */
+ public void setRateLimitFrequencyInSeconds(Integer rateLimitFrequencyInSeconds) {
+ this.rateLimitFrequencyInSeconds = rateLimitFrequencyInSeconds;
+ }
+
+ /**
+ * @param concurrentExecLimit Limit of number of concurrent task that can be IN_PROGRESS at a
+ * given time. Seting the value to 0 removes the limit.
+ */
+ public void setConcurrentExecLimit(Integer concurrentExecLimit) {
+ this.concurrentExecLimit = concurrentExecLimit;
+ }
+
+ /**
+ * @return Limit of number of concurrent task that can be IN_PROGRESS at a given time
+ */
+ public Integer getConcurrentExecLimit() {
+ return concurrentExecLimit;
+ }
+
+ /**
+ * @return concurrency limit
+ */
+ public int concurrencyLimit() {
+ return concurrentExecLimit == null ? 0 : concurrentExecLimit;
+ }
+
+ /**
+ * @param inputTemplate the inputTemplate to set
+ */
+ public void setInputTemplate(Map inputTemplate) {
+ this.inputTemplate = inputTemplate;
+ }
+
+ public String getIsolationGroupId() {
+ return isolationGroupId;
+ }
+
+ public void setIsolationGroupId(String isolationGroupId) {
+ this.isolationGroupId = isolationGroupId;
+ }
+
+ public String getExecutionNameSpace() {
+ return executionNameSpace;
+ }
+
+ public void setExecutionNameSpace(String executionNameSpace) {
+ this.executionNameSpace = executionNameSpace;
+ }
+
+ /**
+ * @return the email of the owner of this task definition
+ */
+ public String getOwnerEmail() {
+ return ownerEmail;
+ }
+
+ /**
+ * @param ownerEmail the owner email to set
+ */
+ public void setOwnerEmail(String ownerEmail) {
+ this.ownerEmail = ownerEmail;
+ }
+
+ /**
+ * @param pollTimeoutSeconds the poll timeout to set
+ */
+ public void setPollTimeoutSeconds(Integer pollTimeoutSeconds) {
+ this.pollTimeoutSeconds = pollTimeoutSeconds;
+ }
+
+ /**
+ * @return the poll timeout of this task definition
+ */
+ public Integer getPollTimeoutSeconds() {
+ return pollTimeoutSeconds;
+ }
+
+ /**
+ * @param backoffScaleFactor the backoff rate to set
+ */
+ public void setBackoffScaleFactor(Integer backoffScaleFactor) {
+ this.backoffScaleFactor = backoffScaleFactor;
+ }
+
+ /**
+ * @return the backoff rate of this task definition
+ */
+ public Integer getBackoffScaleFactor() {
+ return backoffScaleFactor;
+ }
+
+ public String getBaseType() {
+ return baseType;
+ }
+
+ public void setBaseType(String baseType) {
+ this.baseType = baseType;
+ }
+
+ public SchemaDef getInputSchema() {
+ return inputSchema;
+ }
+
+ public void setInputSchema(SchemaDef inputSchema) {
+ this.inputSchema = inputSchema;
+ }
+
+ public SchemaDef getOutputSchema() {
+ return outputSchema;
+ }
+
+ public void setOutputSchema(SchemaDef outputSchema) {
+ this.outputSchema = outputSchema;
+ }
+
+ public boolean isEnforceSchema() {
+ return enforceSchema;
+ }
+
+ public void setEnforceSchema(boolean enforceSchema) {
+ this.enforceSchema = enforceSchema;
+ }
+
+ public String toString() {
+ return name;
+ }
+
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ TaskDef taskDef = (TaskDef) o;
+ return getRetryCount() == taskDef.getRetryCount() && getTimeoutSeconds() == taskDef.getTimeoutSeconds() && getRetryDelaySeconds() == taskDef.getRetryDelaySeconds() && getBackoffScaleFactor() == taskDef.getBackoffScaleFactor() && getResponseTimeoutSeconds() == taskDef.getResponseTimeoutSeconds() && Objects.equals(getName(), taskDef.getName()) && Objects.equals(getDescription(), taskDef.getDescription()) && Objects.equals(getInputKeys(), taskDef.getInputKeys()) && Objects.equals(getOutputKeys(), taskDef.getOutputKeys()) && getTimeoutPolicy() == taskDef.getTimeoutPolicy() && getRetryLogic() == taskDef.getRetryLogic() && Objects.equals(getConcurrentExecLimit(), taskDef.getConcurrentExecLimit()) && Objects.equals(getRateLimitPerFrequency(), taskDef.getRateLimitPerFrequency()) && Objects.equals(getInputTemplate(), taskDef.getInputTemplate()) && Objects.equals(getIsolationGroupId(), taskDef.getIsolationGroupId()) && Objects.equals(getExecutionNameSpace(), taskDef.getExecutionNameSpace()) && Objects.equals(getOwnerEmail(), taskDef.getOwnerEmail()) && Objects.equals(getBaseType(), taskDef.getBaseType()) && Objects.equals(getInputSchema(), taskDef.getInputSchema()) && Objects.equals(getOutputSchema(), taskDef.getOutputSchema());
+ }
+
+ public int hashCode() {
+ return Objects.hash(getName(), getDescription(), getRetryCount(), getTimeoutSeconds(), getInputKeys(), getOutputKeys(), getTimeoutPolicy(), getRetryLogic(), getRetryDelaySeconds(), getBackoffScaleFactor(), getResponseTimeoutSeconds(), getConcurrentExecLimit(), getRateLimitPerFrequency(), getInputTemplate(), getIsolationGroupId(), getExecutionNameSpace(), getOwnerEmail(), getBaseType(), getInputSchema(), getOutputSchema());
+ }
+}
diff --git a/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/common/metadata/tasks/TaskExecLog.java b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/common/metadata/tasks/TaskExecLog.java
new file mode 100644
index 000000000..5c0fa47ee
--- /dev/null
+++ b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/common/metadata/tasks/TaskExecLog.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2020 Conductor 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 com.netflix.conductor.common.metadata.tasks;
+
+import java.util.Objects;
+
+/**
+ * Model that represents the task's execution log.
+ */
+public class TaskExecLog {
+
+ private String log;
+
+ private String taskId;
+
+ private long createdTime;
+
+ public TaskExecLog() {
+ }
+
+ public TaskExecLog(String log) {
+ this.log = log;
+ this.createdTime = System.currentTimeMillis();
+ }
+
+ /**
+ * @return Task Exec Log
+ */
+ public String getLog() {
+ return log;
+ }
+
+ /**
+ * @param log The Log
+ */
+ public void setLog(String log) {
+ this.log = log;
+ }
+
+ /**
+ * @return the taskId
+ */
+ public String getTaskId() {
+ return taskId;
+ }
+
+ /**
+ * @param taskId the taskId to set
+ */
+ public void setTaskId(String taskId) {
+ this.taskId = taskId;
+ }
+
+ /**
+ * @return the createdTime
+ */
+ public long getCreatedTime() {
+ return createdTime;
+ }
+
+ /**
+ * @param createdTime the createdTime to set
+ */
+ public void setCreatedTime(long createdTime) {
+ this.createdTime = createdTime;
+ }
+
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ TaskExecLog that = (TaskExecLog) o;
+ return createdTime == that.createdTime && Objects.equals(log, that.log) && Objects.equals(taskId, that.taskId);
+ }
+
+ public int hashCode() {
+ return Objects.hash(log, taskId, createdTime);
+ }
+}
diff --git a/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/common/metadata/tasks/TaskResult.java b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/common/metadata/tasks/TaskResult.java
new file mode 100644
index 000000000..11b0df281
--- /dev/null
+++ b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/common/metadata/tasks/TaskResult.java
@@ -0,0 +1,263 @@
+/*
+ * Copyright 2022 Conductor 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 com.netflix.conductor.common.metadata.tasks;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import org.apache.commons.lang3.StringUtils;
+
+/**
+ * Result of the task execution.
+ */
+public class TaskResult {
+
+ public enum Status {
+
+ IN_PROGRESS, FAILED, FAILED_WITH_TERMINAL_ERROR, COMPLETED
+ }
+
+ private String workflowInstanceId;
+
+ private String taskId;
+
+ private String reasonForIncompletion;
+
+ private long callbackAfterSeconds;
+
+ private String workerId;
+
+ private Status status;
+
+ private Map outputData = new HashMap<>();
+
+ private List logs = new CopyOnWriteArrayList<>();
+
+ private String externalOutputPayloadStoragePath;
+
+ private String subWorkflowId;
+
+ private boolean extendLease;
+
+ public TaskResult(Task task) {
+ this.workflowInstanceId = task.getWorkflowInstanceId();
+ this.taskId = task.getTaskId();
+ this.reasonForIncompletion = task.getReasonForIncompletion();
+ this.callbackAfterSeconds = task.getCallbackAfterSeconds();
+ this.workerId = task.getWorkerId();
+ this.outputData = task.getOutputData();
+ this.externalOutputPayloadStoragePath = task.getExternalOutputPayloadStoragePath();
+ this.subWorkflowId = task.getSubWorkflowId();
+ switch(task.getStatus()) {
+ case CANCELED:
+ case COMPLETED_WITH_ERRORS:
+ case TIMED_OUT:
+ case SKIPPED:
+ this.status = Status.FAILED;
+ break;
+ case SCHEDULED:
+ this.status = Status.IN_PROGRESS;
+ break;
+ default:
+ this.status = Status.valueOf(task.getStatus().name());
+ break;
+ }
+ }
+
+ public TaskResult() {
+ }
+
+ /**
+ * @return Workflow instance id for which the task result is produced
+ */
+ public String getWorkflowInstanceId() {
+ return workflowInstanceId;
+ }
+
+ public void setWorkflowInstanceId(String workflowInstanceId) {
+ this.workflowInstanceId = workflowInstanceId;
+ }
+
+ public String getTaskId() {
+ return taskId;
+ }
+
+ public void setTaskId(String taskId) {
+ this.taskId = taskId;
+ }
+
+ public String getReasonForIncompletion() {
+ return reasonForIncompletion;
+ }
+
+ public void setReasonForIncompletion(String reasonForIncompletion) {
+ this.reasonForIncompletion = StringUtils.substring(reasonForIncompletion, 0, 500);
+ }
+
+ public long getCallbackAfterSeconds() {
+ return callbackAfterSeconds;
+ }
+
+ /**
+ * When set to non-zero values, the task remains in the queue for the specified seconds before
+ * sent back to the worker when polled. Useful for the long running task, where the task is
+ * updated as IN_PROGRESS and should not be polled out of the queue for a specified amount of
+ * time. (delayed queue implementation)
+ *
+ * @param callbackAfterSeconds Amount of time in seconds the task should be held in the queue
+ * before giving it to a polling worker.
+ */
+ public void setCallbackAfterSeconds(long callbackAfterSeconds) {
+ this.callbackAfterSeconds = callbackAfterSeconds;
+ }
+
+ public String getWorkerId() {
+ return workerId;
+ }
+
+ /**
+ * @param workerId a free form string identifying the worker host. Could be hostname, IP Address
+ * or any other meaningful identifier that can help identify the host/process which executed
+ * the task, in case of troubleshooting.
+ */
+ public void setWorkerId(String workerId) {
+ this.workerId = workerId;
+ }
+
+ /**
+ * @return the status
+ */
+ public Status getStatus() {
+ return status;
+ }
+
+ /**
+ * @param status Status of the task
+ * IN_PROGRESS: Use this for long running tasks, indicating the task is still in
+ * progress and should be checked again at a later time. e.g. the worker checks the status
+ * of the job in the DB, while the job is being executed by another process.
+ *
FAILED, FAILED_WITH_TERMINAL_ERROR, COMPLETED: Terminal statuses for the task.
+ * Use FAILED_WITH_TERMINAL_ERROR when you do not want the task to be retried.
+ * @see #setCallbackAfterSeconds(long)
+ */
+ public void setStatus(Status status) {
+ this.status = status;
+ }
+
+ public Map getOutputData() {
+ return outputData;
+ }
+
+ /**
+ * @param outputData output data to be set for the task execution result
+ */
+ public void setOutputData(Map outputData) {
+ this.outputData = outputData;
+ }
+
+ /**
+ * Adds output
+ *
+ * @param key output field
+ * @param value value
+ * @return current instance
+ */
+ public TaskResult addOutputData(String key, Object value) {
+ this.outputData.put(key, value);
+ return this;
+ }
+
+ /**
+ * @return Task execution logs
+ */
+ public List getLogs() {
+ return logs;
+ }
+
+ /**
+ * @param logs Task execution logs
+ */
+ public void setLogs(List logs) {
+ this.logs = logs;
+ }
+
+ /**
+ * @param log Log line to be added
+ * @return Instance of TaskResult
+ */
+ public TaskResult log(String log) {
+ this.logs.add(new TaskExecLog(log));
+ return this;
+ }
+
+ /**
+ * @return the path where the task output is stored in external storage
+ */
+ public String getExternalOutputPayloadStoragePath() {
+ return externalOutputPayloadStoragePath;
+ }
+
+ /**
+ * @param externalOutputPayloadStoragePath path in the external storage where the task output is
+ * stored
+ */
+ public void setExternalOutputPayloadStoragePath(String externalOutputPayloadStoragePath) {
+ this.externalOutputPayloadStoragePath = externalOutputPayloadStoragePath;
+ }
+
+ public String getSubWorkflowId() {
+ return subWorkflowId;
+ }
+
+ public void setSubWorkflowId(String subWorkflowId) {
+ this.subWorkflowId = subWorkflowId;
+ }
+
+ public boolean isExtendLease() {
+ return extendLease;
+ }
+
+ public void setExtendLease(boolean extendLease) {
+ this.extendLease = extendLease;
+ }
+
+ public String toString() {
+ return "TaskResult{" + "workflowInstanceId='" + workflowInstanceId + '\'' + ", taskId='" + taskId + '\'' + ", reasonForIncompletion='" + reasonForIncompletion + '\'' + ", callbackAfterSeconds=" + callbackAfterSeconds + ", workerId='" + workerId + '\'' + ", status=" + status + ", outputData=" + outputData + ", logs=" + logs + ", externalOutputPayloadStoragePath='" + externalOutputPayloadStoragePath + '\'' + ", subWorkflowId='" + subWorkflowId + '\'' + ", extendLease='" + extendLease + '\'' + '}';
+ }
+
+ public static TaskResult complete() {
+ return newTaskResult(Status.COMPLETED);
+ }
+
+ public static TaskResult failed() {
+ return newTaskResult(Status.FAILED);
+ }
+
+ public static TaskResult failed(String failureReason) {
+ TaskResult result = newTaskResult(Status.FAILED);
+ result.setReasonForIncompletion(failureReason);
+ return result;
+ }
+
+ public static TaskResult inProgress() {
+ return newTaskResult(Status.IN_PROGRESS);
+ }
+
+ public static TaskResult newTaskResult(Status status) {
+ TaskResult result = new TaskResult();
+ result.setStatus(status);
+ return result;
+ }
+}
diff --git a/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/common/metadata/tasks/TaskType.java b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/common/metadata/tasks/TaskType.java
new file mode 100644
index 000000000..a9322e89b
--- /dev/null
+++ b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/common/metadata/tasks/TaskType.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2021 Conductor 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 com.netflix.conductor.common.metadata.tasks;
+
+import java.util.HashSet;
+import java.util.Set;
+
+public enum TaskType {
+
+ SIMPLE,
+ DYNAMIC,
+ FORK_JOIN,
+ FORK_JOIN_DYNAMIC,
+ DECISION,
+ SWITCH,
+ JOIN,
+ DO_WHILE,
+ SUB_WORKFLOW,
+ START_WORKFLOW,
+ EVENT,
+ WAIT,
+ HUMAN,
+ USER_DEFINED,
+ HTTP,
+ LAMBDA,
+ INLINE,
+ EXCLUSIVE_JOIN,
+ TERMINATE,
+ KAFKA_PUBLISH,
+ JSON_JQ_TRANSFORM,
+ SET_VARIABLE,
+ NOOP;
+
+ /**
+ * TaskType constants representing each of the possible enumeration values. Motivation: to not
+ * have any hardcoded/inline strings used in the code.
+ */
+ public static final String TASK_TYPE_DECISION = "DECISION";
+
+ public static final String TASK_TYPE_SWITCH = "SWITCH";
+
+ public static final String TASK_TYPE_DYNAMIC = "DYNAMIC";
+
+ public static final String TASK_TYPE_JOIN = "JOIN";
+
+ public static final String TASK_TYPE_DO_WHILE = "DO_WHILE";
+
+ public static final String TASK_TYPE_FORK_JOIN_DYNAMIC = "FORK_JOIN_DYNAMIC";
+
+ public static final String TASK_TYPE_EVENT = "EVENT";
+
+ public static final String TASK_TYPE_WAIT = "WAIT";
+
+ public static final String TASK_TYPE_HUMAN = "HUMAN";
+
+ public static final String TASK_TYPE_SUB_WORKFLOW = "SUB_WORKFLOW";
+
+ public static final String TASK_TYPE_START_WORKFLOW = "START_WORKFLOW";
+
+ public static final String TASK_TYPE_FORK_JOIN = "FORK_JOIN";
+
+ public static final String TASK_TYPE_SIMPLE = "SIMPLE";
+
+ public static final String TASK_TYPE_HTTP = "HTTP";
+
+ public static final String TASK_TYPE_LAMBDA = "LAMBDA";
+
+ public static final String TASK_TYPE_INLINE = "INLINE";
+
+ public static final String TASK_TYPE_EXCLUSIVE_JOIN = "EXCLUSIVE_JOIN";
+
+ public static final String TASK_TYPE_TERMINATE = "TERMINATE";
+
+ public static final String TASK_TYPE_KAFKA_PUBLISH = "KAFKA_PUBLISH";
+
+ public static final String TASK_TYPE_JSON_JQ_TRANSFORM = "JSON_JQ_TRANSFORM";
+
+ public static final String TASK_TYPE_SET_VARIABLE = "SET_VARIABLE";
+
+ public static final String TASK_TYPE_FORK = "FORK";
+
+ public static final String TASK_TYPE_NOOP = "NOOP";
+
+ private static final Set BUILT_IN_TASKS = new HashSet<>();
+
+ static {
+ BUILT_IN_TASKS.add(TASK_TYPE_DECISION);
+ BUILT_IN_TASKS.add(TASK_TYPE_SWITCH);
+ BUILT_IN_TASKS.add(TASK_TYPE_FORK);
+ BUILT_IN_TASKS.add(TASK_TYPE_JOIN);
+ BUILT_IN_TASKS.add(TASK_TYPE_EXCLUSIVE_JOIN);
+ BUILT_IN_TASKS.add(TASK_TYPE_DO_WHILE);
+ }
+
+ /**
+ * Converts a task type string to {@link TaskType}. For an unknown string, the value is
+ * defaulted to {@link TaskType#USER_DEFINED}.
+ *
+ * NOTE: Use {@link Enum#valueOf(Class, String)} if the default of USER_DEFINED is not
+ * necessary.
+ *
+ * @param taskType The task type string.
+ * @return The {@link TaskType} enum.
+ */
+ public static TaskType of(String taskType) {
+ try {
+ return TaskType.valueOf(taskType);
+ } catch (IllegalArgumentException iae) {
+ return TaskType.USER_DEFINED;
+ }
+ }
+
+ public static boolean isBuiltIn(String taskType) {
+ return BUILT_IN_TASKS.contains(taskType);
+ }
+}
diff --git a/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/common/metadata/workflow/DynamicForkJoinTask.java b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/common/metadata/workflow/DynamicForkJoinTask.java
new file mode 100644
index 000000000..eb488eb49
--- /dev/null
+++ b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/common/metadata/workflow/DynamicForkJoinTask.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2021 Conductor 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 com.netflix.conductor.common.metadata.workflow;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import com.netflix.conductor.common.metadata.tasks.TaskType;
+
+public class DynamicForkJoinTask {
+
+ private String taskName;
+
+ private String workflowName;
+
+ private String referenceName;
+
+ private Map input = new HashMap<>();
+
+ private String type = TaskType.SIMPLE.name();
+
+ public DynamicForkJoinTask() {
+ }
+
+ public DynamicForkJoinTask(String taskName, String workflowName, String referenceName, Map input) {
+ super();
+ this.taskName = taskName;
+ this.workflowName = workflowName;
+ this.referenceName = referenceName;
+ this.input = input;
+ }
+
+ public DynamicForkJoinTask(String taskName, String workflowName, String referenceName, String type, Map input) {
+ super();
+ this.taskName = taskName;
+ this.workflowName = workflowName;
+ this.referenceName = referenceName;
+ this.input = input;
+ this.type = type;
+ }
+
+ public String getTaskName() {
+ return taskName;
+ }
+
+ public void setTaskName(String taskName) {
+ this.taskName = taskName;
+ }
+
+ public String getWorkflowName() {
+ return workflowName;
+ }
+
+ public void setWorkflowName(String workflowName) {
+ this.workflowName = workflowName;
+ }
+
+ public String getReferenceName() {
+ return referenceName;
+ }
+
+ public void setReferenceName(String referenceName) {
+ this.referenceName = referenceName;
+ }
+
+ public Map getInput() {
+ return input;
+ }
+
+ public void setInput(Map input) {
+ this.input = input;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+}
diff --git a/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/common/metadata/workflow/DynamicForkJoinTaskList.java b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/common/metadata/workflow/DynamicForkJoinTaskList.java
new file mode 100644
index 000000000..786161ff6
--- /dev/null
+++ b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/common/metadata/workflow/DynamicForkJoinTaskList.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2020 Conductor 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 com.netflix.conductor.common.metadata.workflow;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+public class DynamicForkJoinTaskList {
+
+ private List dynamicTasks = new ArrayList<>();
+
+ public void add(String taskName, String workflowName, String referenceName, Map input) {
+ dynamicTasks.add(new DynamicForkJoinTask(taskName, workflowName, referenceName, input));
+ }
+
+ public void add(DynamicForkJoinTask dtask) {
+ dynamicTasks.add(dtask);
+ }
+
+ public List getDynamicTasks() {
+ return dynamicTasks;
+ }
+
+ public void setDynamicTasks(List dynamicTasks) {
+ this.dynamicTasks = dynamicTasks;
+ }
+}
diff --git a/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/common/metadata/workflow/IdempotencyStrategy.java b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/common/metadata/workflow/IdempotencyStrategy.java
new file mode 100644
index 000000000..f0d1ef5fa
--- /dev/null
+++ b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/common/metadata/workflow/IdempotencyStrategy.java
@@ -0,0 +1,18 @@
+/*
+ * Copyright 2020 Conductor 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 com.netflix.conductor.common.metadata.workflow;
+
+public enum IdempotencyStrategy {
+
+ FAIL, RETURN_EXISTING
+}
diff --git a/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/common/metadata/workflow/RateLimitConfig.java b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/common/metadata/workflow/RateLimitConfig.java
new file mode 100644
index 000000000..b6d087a9b
--- /dev/null
+++ b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/common/metadata/workflow/RateLimitConfig.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2023 Conductor 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 com.netflix.conductor.common.metadata.workflow;
+
+/**
+ * Rate limit configuration for workflows
+ */
+public class RateLimitConfig {
+
+ /**
+ * Key that defines the rate limit. Rate limit key is a combination of workflow payload such as
+ * name, or correlationId etc.
+ */
+ private String rateLimitKey;
+
+ /**
+ * Number of concurrently running workflows that are allowed per key
+ */
+ private int concurrentExecLimit;
+
+ public String getRateLimitKey() {
+ return rateLimitKey;
+ }
+
+ public void setRateLimitKey(String rateLimitKey) {
+ this.rateLimitKey = rateLimitKey;
+ }
+
+ public int getConcurrentExecLimit() {
+ return concurrentExecLimit;
+ }
+
+ public void setConcurrentExecLimit(int concurrentExecLimit) {
+ this.concurrentExecLimit = concurrentExecLimit;
+ }
+}
diff --git a/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/common/metadata/workflow/RerunWorkflowRequest.java b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/common/metadata/workflow/RerunWorkflowRequest.java
new file mode 100644
index 000000000..65c47d1e9
--- /dev/null
+++ b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/common/metadata/workflow/RerunWorkflowRequest.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2020 Conductor 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 com.netflix.conductor.common.metadata.workflow;
+
+import java.util.Map;
+
+public class RerunWorkflowRequest {
+
+ private String reRunFromWorkflowId;
+
+ private Map workflowInput;
+
+ private String reRunFromTaskId;
+
+ private Map taskInput;
+
+ private String correlationId;
+
+ public String getReRunFromWorkflowId() {
+ return reRunFromWorkflowId;
+ }
+
+ public void setReRunFromWorkflowId(String reRunFromWorkflowId) {
+ this.reRunFromWorkflowId = reRunFromWorkflowId;
+ }
+
+ public Map getWorkflowInput() {
+ return workflowInput;
+ }
+
+ public void setWorkflowInput(Map workflowInput) {
+ this.workflowInput = workflowInput;
+ }
+
+ public String getReRunFromTaskId() {
+ return reRunFromTaskId;
+ }
+
+ public void setReRunFromTaskId(String reRunFromTaskId) {
+ this.reRunFromTaskId = reRunFromTaskId;
+ }
+
+ public Map getTaskInput() {
+ return taskInput;
+ }
+
+ public void setTaskInput(Map taskInput) {
+ this.taskInput = taskInput;
+ }
+
+ public String getCorrelationId() {
+ return correlationId;
+ }
+
+ public void setCorrelationId(String correlationId) {
+ this.correlationId = correlationId;
+ }
+}
diff --git a/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/common/metadata/workflow/SkipTaskRequest.java b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/common/metadata/workflow/SkipTaskRequest.java
new file mode 100644
index 000000000..807bea8f1
--- /dev/null
+++ b/conductor-clients/java/conductor-java-sdk/conductor-client/src/main/java/com/netflix/conductor/common/metadata/workflow/SkipTaskRequest.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2020 Conductor 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 com.netflix.conductor.common.metadata.workflow;
+
+import java.util.Map;
+
+public class SkipTaskRequest {
+
+ private Map taskInput;
+
+ private Map taskOutput;
+
+ public Map getTaskInput() {
+ return taskInput;
+ }
+
+ public void setTaskInput(Map taskInput) {
+ this.taskInput = taskInput;
+ }
+
+ public Map getTaskOutput() {
+ return taskOutput;
+ }
+
+ public void setTaskOutput(Map