From 51d210fb276d21c5eb06df3ff9045e630a177050 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 22 Jan 2025 22:16:43 -0800 Subject: [PATCH 01/53] Add gradle task to update submodule and update proto to latest version --- client/build.gradle | 17 +++++++++++++++++ submodules/durabletask-protobuf | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/client/build.gradle b/client/build.gradle index 3588b3ac..3c66738b 100644 --- a/client/build.gradle +++ b/client/build.gradle @@ -49,12 +49,29 @@ compileTestJava { options.forkOptions.executable = "${PATH_TO_TEST_JAVA_RUNTIME}/bin/javac" } +task updateProtoSubmodule { + description 'Updates the durabletask-protobuf submodule to latest main branch' + group 'Protocol Buffers' + + doLast { + exec { + workingDir '../submodules/durabletask-protobuf' + commandLine 'git', 'fetch', 'origin', 'main' + } + exec { + workingDir '../submodules/durabletask-protobuf' + commandLine 'git', 'checkout', 'origin/main' + } + } +} + protobuf { protoc { artifact = "com.google.protobuf:protoc:${protocVersion}" } plugins { grpc { artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}" } } generateProtoTasks { + all()*.dependsOn updateProtoSubmodule all()*.plugins { grpc {} } } } diff --git a/submodules/durabletask-protobuf b/submodules/durabletask-protobuf index 7d682688..443b333f 160000 --- a/submodules/durabletask-protobuf +++ b/submodules/durabletask-protobuf @@ -1 +1 @@ -Subproject commit 7d6826889eb9b104592ab1020c648517a155ba79 +Subproject commit 443b333f4f65a438dc9eb4f090560d232afec4b7 From 8660f1b62c925c3d20775d9318bbbf368fd555fe Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 22 Jan 2025 22:25:39 -0800 Subject: [PATCH 02/53] bump build pipeline github action version to non deprecated version --- .github/workflows/build-validation.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-validation.yml b/.github/workflows/build-validation.yml index 9262f86d..83fd4608 100644 --- a/.github/workflows/build-validation.yml +++ b/.github/workflows/build-validation.yml @@ -58,13 +58,13 @@ jobs: run: ./gradlew integrationTest - name: Archive test report - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: Integration test report path: client/build/reports/tests/integrationTest - name: Upload JAR output - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: Package path: client/build/libs @@ -105,7 +105,7 @@ jobs: arguments: endToEndTest - name: Archive test report - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: Integration test report path: client/build/reports/tests/endToEndTest @@ -146,7 +146,7 @@ jobs: arguments: sampleTest - name: Archive test report - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: Integration test report path: client/build/reports/tests/endToEndTest \ No newline at end of file From 8a54ce45b6d75d90e3363b0567e0b48e57eb3c40 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 23 Jan 2025 00:48:03 -0800 Subject: [PATCH 03/53] initial dts support changes --- client/build.gradle | 2 + .../durabletask/AccessTokenCache.java | 32 ++ .../durabletask/DurableTaskGrpcWorker.java | 4 +- .../durabletask/OrchestrationRunner.java | 1 + samples/build.gradle | 15 + .../samples/OrchestrationController.java | 88 ++--- .../samples/ReadinessController.java | 15 + .../samples/SpringDurableTaskSample.java | 304 ++++++++++++++++++ .../durabletask/samples/WebApplication.java | 158 ++++----- .../src/main/resources/application.properties | 6 + 10 files changed, 501 insertions(+), 124 deletions(-) create mode 100644 client/src/main/java/com/microsoft/durabletask/AccessTokenCache.java create mode 100644 samples/src/main/java/io/durabletask/samples/ReadinessController.java create mode 100644 samples/src/main/java/io/durabletask/samples/SpringDurableTaskSample.java create mode 100644 samples/src/main/resources/application.properties diff --git a/client/build.gradle b/client/build.gradle index 3c66738b..f7b9d28f 100644 --- a/client/build.gradle +++ b/client/build.gradle @@ -34,6 +34,8 @@ dependencies { implementation "com.fasterxml.jackson.core:jackson-annotations:${jacksonVersion}" implementation "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:${jacksonVersion}" + implementation 'com.azure:azure-identity:1.15.0' + testImplementation(platform('org.junit:junit-bom:5.7.2')) testImplementation('org.junit.jupiter:junit-jupiter') } diff --git a/client/src/main/java/com/microsoft/durabletask/AccessTokenCache.java b/client/src/main/java/com/microsoft/durabletask/AccessTokenCache.java new file mode 100644 index 00000000..bf6cad7c --- /dev/null +++ b/client/src/main/java/com/microsoft/durabletask/AccessTokenCache.java @@ -0,0 +1,32 @@ +package com.microsoft.durabletask; + +import com.azure.core.credential.TokenCredential; +import com.azure.core.credential.AccessToken; +import com.azure.core.credential.TokenRequestContext; + +import java.time.Duration; +import java.time.OffsetDateTime; + +public final class AccessTokenCache { + private final TokenCredential credential; + private final TokenRequestContext context; + private final Duration margin; + private AccessToken cachedToken; + + public AccessTokenCache(TokenCredential credential, TokenRequestContext context, Duration margin) { + this.credential = credential; + this.context = context; + this.margin = margin; + } + + public AccessToken getToken() { + OffsetDateTime nowWithMargin = OffsetDateTime.now().plus(margin); + + if (cachedToken == null + || cachedToken.getExpiresAt().isBefore(nowWithMargin)) { + this.cachedToken = credential.getToken(context).block(); + } + + return cachedToken; + } +} \ No newline at end of file diff --git a/client/src/main/java/com/microsoft/durabletask/DurableTaskGrpcWorker.java b/client/src/main/java/com/microsoft/durabletask/DurableTaskGrpcWorker.java index 92e2bda4..94354e29 100644 --- a/client/src/main/java/com/microsoft/durabletask/DurableTaskGrpcWorker.java +++ b/client/src/main/java/com/microsoft/durabletask/DurableTaskGrpcWorker.java @@ -140,6 +140,7 @@ public void startAndBlock() { .setInstanceId(orchestratorRequest.getInstanceId()) .addAllActions(taskOrchestratorResult.getActions()) .setCustomStatus(StringValue.of(taskOrchestratorResult.getCustomStatus())) + .setCompletionToken(workItem.getCompletionToken()) .build(); this.sidecarClient.completeOrchestratorTask(response); @@ -164,7 +165,8 @@ public void startAndBlock() { ActivityResponse.Builder responseBuilder = ActivityResponse.newBuilder() .setInstanceId(activityRequest.getOrchestrationInstance().getInstanceId()) - .setTaskId(activityRequest.getTaskId()); + .setTaskId(activityRequest.getTaskId()) + .setCompletionToken(workItem.getCompletionToken()); if (output != null) { responseBuilder.setResult(StringValue.of(output)); diff --git a/client/src/main/java/com/microsoft/durabletask/OrchestrationRunner.java b/client/src/main/java/com/microsoft/durabletask/OrchestrationRunner.java index 13904901..66f4f91b 100644 --- a/client/src/main/java/com/microsoft/durabletask/OrchestrationRunner.java +++ b/client/src/main/java/com/microsoft/durabletask/OrchestrationRunner.java @@ -140,6 +140,7 @@ public TaskOrchestration create() { .setInstanceId(orchestratorRequest.getInstanceId()) .addAllActions(taskOrchestratorResult.getActions()) .setCustomStatus(StringValue.of(taskOrchestratorResult.getCustomStatus())) + .setCompletionToken(null) .build(); return response.toByteArray(); } diff --git a/samples/build.gradle b/samples/build.gradle index af90f719..ff421c48 100644 --- a/samples/build.gradle +++ b/samples/build.gradle @@ -6,12 +6,27 @@ plugins { group 'io.durabletask' version = '0.1.0' +def grpcVersion = '1.59.0' archivesBaseName = 'durabletask-samples' +application { + mainClass = 'io.durabletask.samples.SpringDurableTaskSample' +} + dependencies { implementation project(':client') implementation 'org.springframework.boot:spring-boot-starter-web' implementation platform("org.springframework.boot:spring-boot-dependencies:2.5.2") implementation 'org.springframework.boot:spring-boot-starter' + + // https://github.com/grpc/grpc-java#download + implementation "io.grpc:grpc-protobuf:${grpcVersion}" + implementation "io.grpc:grpc-stub:${grpcVersion}" + runtimeOnly "io.grpc:grpc-netty-shaded:${grpcVersion}" + implementation 'com.azure:azure-identity:1.15.0' + + // install lombok + annotationProcessor 'org.projectlombok:lombok:1.18.22' + compileOnly 'org.projectlombok:lombok:1.18.22' } \ No newline at end of file diff --git a/samples/src/main/java/io/durabletask/samples/OrchestrationController.java b/samples/src/main/java/io/durabletask/samples/OrchestrationController.java index 43cc7290..61b6afae 100644 --- a/samples/src/main/java/io/durabletask/samples/OrchestrationController.java +++ b/samples/src/main/java/io/durabletask/samples/OrchestrationController.java @@ -1,44 +1,44 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -package io.durabletask.samples; - -import com.microsoft.durabletask.*; - -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -@RestController -public class OrchestrationController { - - final DurableTaskClient client; - - public OrchestrationController() { - this.client = new DurableTaskGrpcClientBuilder().build(); - } - - @GetMapping("/hello") - public String greeting(@RequestParam(value = "name", defaultValue = "World") String name) { - return String.format("Hello, %s!", name); - } - - @GetMapping("/placeOrder") - public NewOrderResponse placeOrder(@RequestParam(value = "item") String item) { - String instanceId = this.client.scheduleNewOrchestrationInstance( - "ProcessOrderOrchestration", - new NewOrchestrationInstanceOptions().setInput(item)); - return new NewOrderResponse(instanceId); - } - - private class NewOrderResponse { - private final String instanceId; - - public NewOrderResponse(String instanceId) { - this.instanceId = instanceId; - } - - public String getInstanceId() { - return this.instanceId; - } - } -} \ No newline at end of file +// // Copyright (c) Microsoft Corporation. All rights reserved. +// // Licensed under the MIT License. +// package io.durabletask.samples; + +// import com.microsoft.durabletask.*; + +// import org.springframework.web.bind.annotation.GetMapping; +// import org.springframework.web.bind.annotation.RequestParam; +// import org.springframework.web.bind.annotation.RestController; + +// @RestController +// public class OrchestrationController { + +// final DurableTaskClient client; + +// public OrchestrationController() { +// this.client = new DurableTaskGrpcClientBuilder().build(); +// } + +// @GetMapping("/hello") +// public String greeting(@RequestParam(value = "name", defaultValue = "World") String name) { +// return String.format("Hello, %s!", name); +// } + +// @GetMapping("/placeOrder") +// public NewOrderResponse placeOrder(@RequestParam(value = "item") String item) { +// String instanceId = this.client.scheduleNewOrchestrationInstance( +// "ProcessOrderOrchestration", +// new NewOrchestrationInstanceOptions().setInput(item)); +// return new NewOrderResponse(instanceId); +// } + +// private class NewOrderResponse { +// private final String instanceId; + +// public NewOrderResponse(String instanceId) { +// this.instanceId = instanceId; +// } + +// public String getInstanceId() { +// return this.instanceId; +// } +// } +// } \ No newline at end of file diff --git a/samples/src/main/java/io/durabletask/samples/ReadinessController.java b/samples/src/main/java/io/durabletask/samples/ReadinessController.java new file mode 100644 index 00000000..9ee26960 --- /dev/null +++ b/samples/src/main/java/io/durabletask/samples/ReadinessController.java @@ -0,0 +1,15 @@ +package io.durabletask.samples; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/ready") +public class ReadinessController { + @GetMapping + public ResponseEntity readiness() { + return ResponseEntity.ok("Application is ready"); + } +} diff --git a/samples/src/main/java/io/durabletask/samples/SpringDurableTaskSample.java b/samples/src/main/java/io/durabletask/samples/SpringDurableTaskSample.java new file mode 100644 index 00000000..b0cddadc --- /dev/null +++ b/samples/src/main/java/io/durabletask/samples/SpringDurableTaskSample.java @@ -0,0 +1,304 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package io.durabletask.samples; + +import com.azure.core.credential.AccessToken; +import com.microsoft.durabletask.*; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.web.bind.annotation.*; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.beans.factory.annotation.Value; +import java.time.Duration; +import io.grpc.ManagedChannelBuilder; +import io.grpc.Channel; +import io.grpc.ClientCall; +import io.grpc.ClientInterceptor; +import io.grpc.ForwardingClientCall; +import io.grpc.Metadata; +import io.grpc.MethodDescriptor; +import io.grpc.CallOptions; +import io.grpc.ChannelCredentials; +import io.grpc.TlsChannelCredentials; +import io.grpc.InsecureChannelCredentials; +import com.azure.core.credential.TokenCredential; +import com.azure.core.credential.TokenRequestContext; +import java.util.Objects; +import com.azure.identity.DefaultAzureCredentialBuilder; +import reactor.core.publisher.Mono; + +@ConfigurationProperties(prefix = "durable.task") +@lombok.Data +class DurableTaskProperties { + private String endpoint; + private String hubName; + private String resourceId = "https://durabletask.io"; + private boolean allowInsecure = false; +} + +/** + * Sample Spring Boot application demonstrating Azure Durable Task integration. + * This sample shows how to: + * 1. Configure Durable Task with Spring Boot + * 2. Create orchestrations and activities + * 3. Handle REST API endpoints for order processing + */ +@SpringBootApplication +@EnableConfigurationProperties(DurableTaskProperties.class) +public class SpringDurableTaskSample { + + public static void main(String[] args) { + ConfigurableApplicationContext context = SpringApplication.run(SpringDurableTaskSample.class, args); + + // Get the worker bean and start it + DurableTaskGrpcWorker worker = context.getBean(DurableTaskGrpcWorker.class); + worker.start(); + } + + @Configuration + static class DurableTaskConfig { + + @Bean + public TokenCredential tokenCredential() { + return new DefaultAzureCredentialBuilder().build(); + } + + @Bean + public AccessTokenCache accessTokenCache( + TokenCredential credential, + DurableTaskProperties properties) { + if (credential == null) { + return null; + } + TokenRequestContext context = new TokenRequestContext(); + context.addScopes(new String[] { properties.getResourceId() + "/.default" }); + return new AccessTokenCache( + credential, context, Duration.ofMinutes(5) + ); + } + + @Bean(name = "workerChannel") + public Channel workerGrpcChannel( + DurableTaskProperties properties, + AccessTokenCache tokenCache) { + return createChannel(properties, tokenCache); + } + + @Bean(name = "clientChannel") + public Channel clientGrpcChannel( + DurableTaskProperties properties, + AccessTokenCache tokenCache) { + return createChannel(properties, tokenCache); + } + + private Channel createChannel(DurableTaskProperties properties, AccessTokenCache tokenCache) { + Objects.requireNonNull(properties.getHubName(), "taskHubName must not be null"); + + // Normalize the endpoint URL and add DNS scheme for gRPC name resolution + String endpoint = "dns:///" + properties.getEndpoint(); + + // Create metadata interceptor to add task hub name and auth token + ClientInterceptor metadataInterceptor = new ClientInterceptor() { + @Override + public ClientCall interceptCall( + MethodDescriptor method, + CallOptions callOptions, + Channel next) { + return new ForwardingClientCall.SimpleForwardingClientCall( + next.newCall(method, callOptions)) { + @Override + public void start(ClientCall.Listener responseListener, Metadata headers) { + headers.put( + Metadata.Key.of("taskhub", Metadata.ASCII_STRING_MARSHALLER), + properties.getHubName() + ); + + // Add authorization token if credentials are configured + if (tokenCache != null) { + String token = tokenCache.getToken().getToken(); + headers.put( + Metadata.Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER), + "Bearer " + token + ); + } + + super.start(responseListener, headers); + } + }; + } + }; + + // Build the channel with appropriate security settings + ManagedChannelBuilder builder = ManagedChannelBuilder.forTarget(endpoint) + .intercept(metadataInterceptor); + + if (!properties.isAllowInsecure()) { + builder.useTransportSecurity(); + } else { + builder.usePlaintext(); + } + + return builder.build(); + } + + @Bean(destroyMethod = "stop") + public DurableTaskGrpcWorker durableTaskWorker(@Qualifier("workerChannel") Channel grpcChannel) { + DurableTaskGrpcWorkerBuilder builder = new DurableTaskGrpcWorkerBuilder() + .grpcChannel(grpcChannel); + + // Add orchestrations + builder.addOrchestration(new TaskOrchestrationFactory() { + @Override + public String getName() { + return "ProcessOrderOrchestration"; + } + + @Override + public TaskOrchestration create() { + return ctx -> { + // Get the order input as JSON string + String orderJson = ctx.getInput(String.class); + + // Process the order through multiple activities + boolean isValid = ctx.callActivity("ValidateOrder", orderJson, Boolean.class).await(); + if (!isValid) { + ctx.complete("{\"status\": \"FAILED\", \"message\": \"Order validation failed\"}"); + return; + } + + // Process payment + String paymentResult = ctx.callActivity("ProcessPayment", orderJson, String.class).await(); + if (!paymentResult.contains("\"success\":true")) { + ctx.complete("{\"status\": \"FAILED\", \"message\": \"Payment processing failed\"}"); + return; + } + + // Ship order + String shipmentResult = ctx.callActivity("ShipOrder", orderJson, String.class).await(); + + // Return the final result + ctx.complete("{\"status\": \"SUCCESS\", " + + "\"payment\": " + paymentResult + ", " + + "\"shipment\": " + shipmentResult + "}"); + }; + } + }); + + // Add activity implementations + builder.addActivity(new TaskActivityFactory() { + @Override + public String getName() { return "ValidateOrder"; } + + @Override + public TaskActivity create() { + return ctx -> { + String orderJson = ctx.getInput(String.class); + // Simple validation - check if order contains amount and it's greater than 0 + return orderJson.contains("\"amount\"") && !orderJson.contains("\"amount\":0"); + }; + } + }); + + builder.addActivity(new TaskActivityFactory() { + @Override + public String getName() { return "ProcessPayment"; } + + @Override + public TaskActivity create() { + return ctx -> { + String orderJson = ctx.getInput(String.class); + // Simulate payment processing + sleep(1000); // Simulate processing time + return "{\"success\":true, \"transactionId\":\"TXN" + System.currentTimeMillis() + "\"}"; + }; + } + }); + + builder.addActivity(new TaskActivityFactory() { + @Override + public String getName() { return "ShipOrder"; } + + @Override + public TaskActivity create() { + return ctx -> { + String orderJson = ctx.getInput(String.class); + // Simulate shipping process + sleep(1000); // Simulate processing time + return "{\"trackingNumber\":\"TRACK" + System.currentTimeMillis() + "\"}"; + }; + } + }); + + return builder.build(); + } + + @Bean + public DurableTaskClient durableTaskClient(@Qualifier("clientChannel") Channel grpcChannel) { + return new DurableTaskGrpcClientBuilder() + .grpcChannel(grpcChannel) + .build(); + } + } + + private static void sleep(int millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + // ignore + } + } +} + +/** + * REST Controller for handling order-related operations. + */ +@RestController +@RequestMapping("/api/orders") +class OrderController { + + private final DurableTaskClient client; + + public OrderController(DurableTaskClient client) { + this.client = client; + } + + @PostMapping + public String createOrder(@RequestBody String orderJson) throws Exception { + String instanceId = client.scheduleNewOrchestrationInstance( + "ProcessOrderOrchestration", + orderJson + ); + + // Wait for the orchestration to complete with a timeout + OrchestrationMetadata metadata = client.waitForInstanceCompletion( + instanceId, + Duration.ofSeconds(30), + true + ); + + if (metadata.getRuntimeStatus() == OrchestrationRuntimeStatus.COMPLETED) { + return metadata.readOutputAs(String.class); + } else { + return "{\"status\": \"" + metadata.getRuntimeStatus() + "\"}"; + } + } + + @GetMapping("/{instanceId}") + public String getOrder(@PathVariable String instanceId) throws Exception { + OrchestrationMetadata metadata = client.getInstanceMetadata(instanceId, true); + if (metadata == null) { + return "{\"error\": \"Order not found\"}"; + } + + if (metadata.getRuntimeStatus() == OrchestrationRuntimeStatus.COMPLETED) { + return metadata.readOutputAs(String.class); + } else { + return "{\"status\": \"" + metadata.getRuntimeStatus() + "\"}"; + } + } +} diff --git a/samples/src/main/java/io/durabletask/samples/WebApplication.java b/samples/src/main/java/io/durabletask/samples/WebApplication.java index b9c4bcc5..27cd12fe 100644 --- a/samples/src/main/java/io/durabletask/samples/WebApplication.java +++ b/samples/src/main/java/io/durabletask/samples/WebApplication.java @@ -1,97 +1,97 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -package io.durabletask.samples; +// // Copyright (c) Microsoft Corporation. All rights reserved. +// // Licensed under the MIT License. +// package io.durabletask.samples; -import com.microsoft.durabletask.*; +// import com.microsoft.durabletask.*; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; +// import org.springframework.boot.SpringApplication; +// import org.springframework.boot.autoconfigure.SpringBootApplication; -@SpringBootApplication -public class WebApplication { +// @SpringBootApplication +// public class WebApplication { - public static void main(String[] args) throws InterruptedException { - DurableTaskGrpcWorker server = createTaskHubServer(); - server.start(); +// public static void main(String[] args) throws InterruptedException { +// DurableTaskGrpcWorker server = createTaskHubServer(); +// server.start(); - System.out.println("Starting up Spring web API..."); - SpringApplication.run(WebApplication.class, args); - } +// System.out.println("Starting up Spring web API..."); +// SpringApplication.run(WebApplication.class, args); +// } - private static DurableTaskGrpcWorker createTaskHubServer() { - DurableTaskGrpcWorkerBuilder builder = new DurableTaskGrpcWorkerBuilder(); +// private static DurableTaskGrpcWorker createTaskHubServer() { +// DurableTaskGrpcWorkerBuilder builder = new DurableTaskGrpcWorkerBuilder(); - // Orchestrations can be defined inline as anonymous classes or as concrete classes - builder.addOrchestration(new TaskOrchestrationFactory() { - @Override - public String getName() { return "ProcessOrderOrchestration"; } +// // Orchestrations can be defined inline as anonymous classes or as concrete classes +// builder.addOrchestration(new TaskOrchestrationFactory() { +// @Override +// public String getName() { return "ProcessOrderOrchestration"; } - @Override - public TaskOrchestration create() { - return ctx -> { - // the input is JSON and can be deserialized into the specified type - String input = ctx.getInput(String.class); +// @Override +// public TaskOrchestration create() { +// return ctx -> { +// // the input is JSON and can be deserialized into the specified type +// String input = ctx.getInput(String.class); - String x = ctx.callActivity("Task1", input, String.class).await(); - String y = ctx.callActivity("Task2", x, String.class).await(); - String z = ctx.callActivity("Task3", y, String.class).await(); +// String x = ctx.callActivity("Task1", input, String.class).await(); +// String y = ctx.callActivity("Task2", x, String.class).await(); +// String z = ctx.callActivity("Task3", y, String.class).await(); - ctx.complete(z); - }; - } - }); +// ctx.complete(z); +// }; +// } +// }); - // Activities can be defined inline as anonymous classes or as concrete classes - builder.addActivity(new TaskActivityFactory() { - @Override - public String getName() { return "Task1"; } +// // Activities can be defined inline as anonymous classes or as concrete classes +// builder.addActivity(new TaskActivityFactory() { +// @Override +// public String getName() { return "Task1"; } - @Override - public TaskActivity create() { - return ctx -> { - String input = ctx.getInput(String.class); - sleep(10000); - return input + "|" + ctx.getName(); - }; - } - }); +// @Override +// public TaskActivity create() { +// return ctx -> { +// String input = ctx.getInput(String.class); +// sleep(10000); +// return input + "|" + ctx.getName(); +// }; +// } +// }); - builder.addActivity(new TaskActivityFactory() { - @Override - public String getName() { return "Task2"; } +// builder.addActivity(new TaskActivityFactory() { +// @Override +// public String getName() { return "Task2"; } - @Override - public TaskActivity create() { - return ctx -> { - String input = ctx.getInput(String.class); - sleep(10000); - return input + "|" + ctx.getName(); - }; - } - }); +// @Override +// public TaskActivity create() { +// return ctx -> { +// String input = ctx.getInput(String.class); +// sleep(10000); +// return input + "|" + ctx.getName(); +// }; +// } +// }); - builder.addActivity(new TaskActivityFactory() { - @Override - public String getName() { return "Task3"; } +// builder.addActivity(new TaskActivityFactory() { +// @Override +// public String getName() { return "Task3"; } - @Override - public TaskActivity create() { - return ctx -> { - String input = ctx.getInput(String.class); - sleep(10000); - return input + "|" + ctx.getName(); - }; - } - }); +// @Override +// public TaskActivity create() { +// return ctx -> { +// String input = ctx.getInput(String.class); +// sleep(10000); +// return input + "|" + ctx.getName(); +// }; +// } +// }); - return builder.build(); - } +// return builder.build(); +// } - private static void sleep(int millis) { - try { - Thread.sleep(millis); - } catch (InterruptedException e) { - // ignore - } - } -} +// private static void sleep(int millis) { +// try { +// Thread.sleep(millis); +// } catch (InterruptedException e) { +// // ignore +// } +// } +// } diff --git a/samples/src/main/resources/application.properties b/samples/src/main/resources/application.properties new file mode 100644 index 00000000..4fd9b3d3 --- /dev/null +++ b/samples/src/main/resources/application.properties @@ -0,0 +1,6 @@ +durable.task.endpoint=wbtestdts02-g7ahczeycua9.westus2.durabletask.io:443 +durable.task.hub-name=wbtb100 +durable.task.allow-insecure=false +# Add logging configuration +logging.level.com.microsoft.durabletask=DEBUG +logging.level.root=INFO \ No newline at end of file From f951428cda80075beed2c6d35333878df3917169 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 23 Jan 2025 11:05:20 -0800 Subject: [PATCH 04/53] placeholder --- samples/src/main/resources/application.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/src/main/resources/application.properties b/samples/src/main/resources/application.properties index 4fd9b3d3..53d6a6d0 100644 --- a/samples/src/main/resources/application.properties +++ b/samples/src/main/resources/application.properties @@ -1,5 +1,5 @@ -durable.task.endpoint=wbtestdts02-g7ahczeycua9.westus2.durabletask.io:443 -durable.task.hub-name=wbtb100 +durable.task.endpoint= +durable.task.hub-name= durable.task.allow-insecure=false # Add logging configuration logging.level.com.microsoft.durabletask=DEBUG From 12e9e500c2f2530b4f48102e647fc57fb1598cfa Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 23 Jan 2025 11:05:57 -0800 Subject: [PATCH 05/53] ignore --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 3ff2e24d..fb0be4ee 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,7 @@ build/ .project .settings .classpath -repo/ \ No newline at end of file +repo/ + +# Ignore sample application properties or any other properties files used for sample +samples/src/main/resources/*.properties \ No newline at end of file From 3b09d59371c9e71182649b1bc94a312f39d4bbd0 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 23 Jan 2025 11:24:07 -0800 Subject: [PATCH 06/53] update --- samples/build.gradle | 2 +- .../samples/OrchestrationController.java | 88 +++++----- .../samples/ReadinessController.java | 15 -- ...> WebAppToDurableTaskSchedulerSample.java} | 13 +- .../durabletask/samples/WebApplication.java | 158 +++++++++--------- 5 files changed, 132 insertions(+), 144 deletions(-) delete mode 100644 samples/src/main/java/io/durabletask/samples/ReadinessController.java rename samples/src/main/java/io/durabletask/samples/{SpringDurableTaskSample.java => WebAppToDurableTaskSchedulerSample.java} (98%) diff --git a/samples/build.gradle b/samples/build.gradle index ff421c48..2312e7d8 100644 --- a/samples/build.gradle +++ b/samples/build.gradle @@ -10,7 +10,7 @@ def grpcVersion = '1.59.0' archivesBaseName = 'durabletask-samples' application { - mainClass = 'io.durabletask.samples.SpringDurableTaskSample' + mainClass = 'io.durabletask.samples.WebAppToDurableTaskSchedulerSample' } dependencies { diff --git a/samples/src/main/java/io/durabletask/samples/OrchestrationController.java b/samples/src/main/java/io/durabletask/samples/OrchestrationController.java index 61b6afae..43cc7290 100644 --- a/samples/src/main/java/io/durabletask/samples/OrchestrationController.java +++ b/samples/src/main/java/io/durabletask/samples/OrchestrationController.java @@ -1,44 +1,44 @@ -// // Copyright (c) Microsoft Corporation. All rights reserved. -// // Licensed under the MIT License. -// package io.durabletask.samples; - -// import com.microsoft.durabletask.*; - -// import org.springframework.web.bind.annotation.GetMapping; -// import org.springframework.web.bind.annotation.RequestParam; -// import org.springframework.web.bind.annotation.RestController; - -// @RestController -// public class OrchestrationController { - -// final DurableTaskClient client; - -// public OrchestrationController() { -// this.client = new DurableTaskGrpcClientBuilder().build(); -// } - -// @GetMapping("/hello") -// public String greeting(@RequestParam(value = "name", defaultValue = "World") String name) { -// return String.format("Hello, %s!", name); -// } - -// @GetMapping("/placeOrder") -// public NewOrderResponse placeOrder(@RequestParam(value = "item") String item) { -// String instanceId = this.client.scheduleNewOrchestrationInstance( -// "ProcessOrderOrchestration", -// new NewOrchestrationInstanceOptions().setInput(item)); -// return new NewOrderResponse(instanceId); -// } - -// private class NewOrderResponse { -// private final String instanceId; - -// public NewOrderResponse(String instanceId) { -// this.instanceId = instanceId; -// } - -// public String getInstanceId() { -// return this.instanceId; -// } -// } -// } \ No newline at end of file +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package io.durabletask.samples; + +import com.microsoft.durabletask.*; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class OrchestrationController { + + final DurableTaskClient client; + + public OrchestrationController() { + this.client = new DurableTaskGrpcClientBuilder().build(); + } + + @GetMapping("/hello") + public String greeting(@RequestParam(value = "name", defaultValue = "World") String name) { + return String.format("Hello, %s!", name); + } + + @GetMapping("/placeOrder") + public NewOrderResponse placeOrder(@RequestParam(value = "item") String item) { + String instanceId = this.client.scheduleNewOrchestrationInstance( + "ProcessOrderOrchestration", + new NewOrchestrationInstanceOptions().setInput(item)); + return new NewOrderResponse(instanceId); + } + + private class NewOrderResponse { + private final String instanceId; + + public NewOrderResponse(String instanceId) { + this.instanceId = instanceId; + } + + public String getInstanceId() { + return this.instanceId; + } + } +} \ No newline at end of file diff --git a/samples/src/main/java/io/durabletask/samples/ReadinessController.java b/samples/src/main/java/io/durabletask/samples/ReadinessController.java deleted file mode 100644 index 9ee26960..00000000 --- a/samples/src/main/java/io/durabletask/samples/ReadinessController.java +++ /dev/null @@ -1,15 +0,0 @@ -package io.durabletask.samples; - -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping("/ready") -public class ReadinessController { - @GetMapping - public ResponseEntity readiness() { - return ResponseEntity.ok("Application is ready"); - } -} diff --git a/samples/src/main/java/io/durabletask/samples/SpringDurableTaskSample.java b/samples/src/main/java/io/durabletask/samples/WebAppToDurableTaskSchedulerSample.java similarity index 98% rename from samples/src/main/java/io/durabletask/samples/SpringDurableTaskSample.java rename to samples/src/main/java/io/durabletask/samples/WebAppToDurableTaskSchedulerSample.java index b0cddadc..1fe9ee2b 100644 --- a/samples/src/main/java/io/durabletask/samples/SpringDurableTaskSample.java +++ b/samples/src/main/java/io/durabletask/samples/WebAppToDurableTaskSchedulerSample.java @@ -10,7 +10,9 @@ import org.springframework.web.bind.annotation.*; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.beans.factory.annotation.Value; @@ -50,10 +52,10 @@ class DurableTaskProperties { */ @SpringBootApplication @EnableConfigurationProperties(DurableTaskProperties.class) -public class SpringDurableTaskSample { +public class WebAppToDurableTaskSchedulerSample { public static void main(String[] args) { - ConfigurableApplicationContext context = SpringApplication.run(SpringDurableTaskSample.class, args); + ConfigurableApplicationContext context = SpringApplication.run(WebAppToDurableTaskSchedulerSample.class, args); // Get the worker bean and start it DurableTaskGrpcWorker worker = context.getBean(DurableTaskGrpcWorker.class); @@ -254,13 +256,14 @@ private static void sleep(int millis) { } } + /** * REST Controller for handling order-related operations. */ @RestController @RequestMapping("/api/orders") class OrderController { - + private final DurableTaskClient client; public OrderController(DurableTaskClient client) { @@ -273,7 +276,7 @@ public String createOrder(@RequestBody String orderJson) throws Exception { "ProcessOrderOrchestration", orderJson ); - + // Wait for the orchestration to complete with a timeout OrchestrationMetadata metadata = client.waitForInstanceCompletion( instanceId, @@ -301,4 +304,4 @@ public String getOrder(@PathVariable String instanceId) throws Exception { return "{\"status\": \"" + metadata.getRuntimeStatus() + "\"}"; } } -} +} \ No newline at end of file diff --git a/samples/src/main/java/io/durabletask/samples/WebApplication.java b/samples/src/main/java/io/durabletask/samples/WebApplication.java index 27cd12fe..b9c4bcc5 100644 --- a/samples/src/main/java/io/durabletask/samples/WebApplication.java +++ b/samples/src/main/java/io/durabletask/samples/WebApplication.java @@ -1,97 +1,97 @@ -// // Copyright (c) Microsoft Corporation. All rights reserved. -// // Licensed under the MIT License. -// package io.durabletask.samples; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package io.durabletask.samples; -// import com.microsoft.durabletask.*; +import com.microsoft.durabletask.*; -// import org.springframework.boot.SpringApplication; -// import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; -// @SpringBootApplication -// public class WebApplication { +@SpringBootApplication +public class WebApplication { -// public static void main(String[] args) throws InterruptedException { -// DurableTaskGrpcWorker server = createTaskHubServer(); -// server.start(); + public static void main(String[] args) throws InterruptedException { + DurableTaskGrpcWorker server = createTaskHubServer(); + server.start(); -// System.out.println("Starting up Spring web API..."); -// SpringApplication.run(WebApplication.class, args); -// } + System.out.println("Starting up Spring web API..."); + SpringApplication.run(WebApplication.class, args); + } -// private static DurableTaskGrpcWorker createTaskHubServer() { -// DurableTaskGrpcWorkerBuilder builder = new DurableTaskGrpcWorkerBuilder(); + private static DurableTaskGrpcWorker createTaskHubServer() { + DurableTaskGrpcWorkerBuilder builder = new DurableTaskGrpcWorkerBuilder(); -// // Orchestrations can be defined inline as anonymous classes or as concrete classes -// builder.addOrchestration(new TaskOrchestrationFactory() { -// @Override -// public String getName() { return "ProcessOrderOrchestration"; } + // Orchestrations can be defined inline as anonymous classes or as concrete classes + builder.addOrchestration(new TaskOrchestrationFactory() { + @Override + public String getName() { return "ProcessOrderOrchestration"; } -// @Override -// public TaskOrchestration create() { -// return ctx -> { -// // the input is JSON and can be deserialized into the specified type -// String input = ctx.getInput(String.class); + @Override + public TaskOrchestration create() { + return ctx -> { + // the input is JSON and can be deserialized into the specified type + String input = ctx.getInput(String.class); -// String x = ctx.callActivity("Task1", input, String.class).await(); -// String y = ctx.callActivity("Task2", x, String.class).await(); -// String z = ctx.callActivity("Task3", y, String.class).await(); + String x = ctx.callActivity("Task1", input, String.class).await(); + String y = ctx.callActivity("Task2", x, String.class).await(); + String z = ctx.callActivity("Task3", y, String.class).await(); -// ctx.complete(z); -// }; -// } -// }); + ctx.complete(z); + }; + } + }); -// // Activities can be defined inline as anonymous classes or as concrete classes -// builder.addActivity(new TaskActivityFactory() { -// @Override -// public String getName() { return "Task1"; } + // Activities can be defined inline as anonymous classes or as concrete classes + builder.addActivity(new TaskActivityFactory() { + @Override + public String getName() { return "Task1"; } -// @Override -// public TaskActivity create() { -// return ctx -> { -// String input = ctx.getInput(String.class); -// sleep(10000); -// return input + "|" + ctx.getName(); -// }; -// } -// }); + @Override + public TaskActivity create() { + return ctx -> { + String input = ctx.getInput(String.class); + sleep(10000); + return input + "|" + ctx.getName(); + }; + } + }); -// builder.addActivity(new TaskActivityFactory() { -// @Override -// public String getName() { return "Task2"; } + builder.addActivity(new TaskActivityFactory() { + @Override + public String getName() { return "Task2"; } -// @Override -// public TaskActivity create() { -// return ctx -> { -// String input = ctx.getInput(String.class); -// sleep(10000); -// return input + "|" + ctx.getName(); -// }; -// } -// }); + @Override + public TaskActivity create() { + return ctx -> { + String input = ctx.getInput(String.class); + sleep(10000); + return input + "|" + ctx.getName(); + }; + } + }); -// builder.addActivity(new TaskActivityFactory() { -// @Override -// public String getName() { return "Task3"; } + builder.addActivity(new TaskActivityFactory() { + @Override + public String getName() { return "Task3"; } -// @Override -// public TaskActivity create() { -// return ctx -> { -// String input = ctx.getInput(String.class); -// sleep(10000); -// return input + "|" + ctx.getName(); -// }; -// } -// }); + @Override + public TaskActivity create() { + return ctx -> { + String input = ctx.getInput(String.class); + sleep(10000); + return input + "|" + ctx.getName(); + }; + } + }); -// return builder.build(); -// } + return builder.build(); + } -// private static void sleep(int millis) { -// try { -// Thread.sleep(millis); -// } catch (InterruptedException e) { -// // ignore -// } -// } -// } + private static void sleep(int millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + // ignore + } + } +} From 32eca1efdbb4deefe1f56712ff5a0d4d91a01755 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 23 Jan 2025 11:26:54 -0800 Subject: [PATCH 07/53] remove --- .../main/java/com/microsoft/durabletask/OrchestrationRunner.java | 1 - 1 file changed, 1 deletion(-) diff --git a/client/src/main/java/com/microsoft/durabletask/OrchestrationRunner.java b/client/src/main/java/com/microsoft/durabletask/OrchestrationRunner.java index 66f4f91b..13904901 100644 --- a/client/src/main/java/com/microsoft/durabletask/OrchestrationRunner.java +++ b/client/src/main/java/com/microsoft/durabletask/OrchestrationRunner.java @@ -140,7 +140,6 @@ public TaskOrchestration create() { .setInstanceId(orchestratorRequest.getInstanceId()) .addAllActions(taskOrchestratorResult.getActions()) .setCustomStatus(StringValue.of(taskOrchestratorResult.getCustomStatus())) - .setCompletionToken(null) .build(); return response.toByteArray(); } From ea2e181f607569d5fcf8c362a4eb5d53eec75514 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 23 Jan 2025 11:51:36 -0800 Subject: [PATCH 08/53] update --- .github/workflows/build-validation.yml | 6 ------ .gitmodules | 3 --- azdevops-pipeline/build-release-artifacts.yml | 1 - client/build.gradle | 19 +------------------ eng/templates/build.yml | 1 - submodules/durabletask-protobuf | 1 - 6 files changed, 1 insertion(+), 30 deletions(-) delete mode 100644 .gitmodules delete mode 160000 submodules/durabletask-protobuf diff --git a/.github/workflows/build-validation.yml b/.github/workflows/build-validation.yml index 83fd4608..9e675001 100644 --- a/.github/workflows/build-validation.yml +++ b/.github/workflows/build-validation.yml @@ -20,8 +20,6 @@ jobs: steps: - uses: actions/checkout@v2 - with: - submodules: true - name: Set up JDK 11 uses: actions/setup-java@v2 @@ -76,8 +74,6 @@ jobs: steps: - uses: actions/checkout@v2 - with: - submodules: true - name: Set up JDK 8 uses: actions/setup-java@v2 @@ -117,8 +113,6 @@ jobs: steps: - uses: actions/checkout@v2 - with: - submodules: true - name: Set up JDK 8 uses: actions/setup-java@v2 diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index b3715165..00000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "submodules/durabletask-protobuf"] - path = submodules/durabletask-protobuf - url = https://github.com/microsoft/durabletask-protobuf diff --git a/azdevops-pipeline/build-release-artifacts.yml b/azdevops-pipeline/build-release-artifacts.yml index e4290786..728dde4b 100644 --- a/azdevops-pipeline/build-release-artifacts.yml +++ b/azdevops-pipeline/build-release-artifacts.yml @@ -10,7 +10,6 @@ pool: steps: - checkout: self - submodules: true - task: Gradle@3 inputs: diff --git a/client/build.gradle b/client/build.gradle index f7b9d28f..e18c2561 100644 --- a/client/build.gradle +++ b/client/build.gradle @@ -51,29 +51,12 @@ compileTestJava { options.forkOptions.executable = "${PATH_TO_TEST_JAVA_RUNTIME}/bin/javac" } -task updateProtoSubmodule { - description 'Updates the durabletask-protobuf submodule to latest main branch' - group 'Protocol Buffers' - - doLast { - exec { - workingDir '../submodules/durabletask-protobuf' - commandLine 'git', 'fetch', 'origin', 'main' - } - exec { - workingDir '../submodules/durabletask-protobuf' - commandLine 'git', 'checkout', 'origin/main' - } - } -} - protobuf { protoc { artifact = "com.google.protobuf:protoc:${protocVersion}" } plugins { grpc { artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}" } } generateProtoTasks { - all()*.dependsOn updateProtoSubmodule all()*.plugins { grpc {} } } } @@ -81,7 +64,7 @@ protobuf { sourceSets { main { proto { - srcDir '../submodules/durabletask-protobuf/protos' + srcDir '../internal/durabletask-protobuf/protos' } java { srcDirs 'build/generated/source/proto/main/grpc' diff --git a/eng/templates/build.yml b/eng/templates/build.yml index 09296c13..9919c6d2 100644 --- a/eng/templates/build.yml +++ b/eng/templates/build.yml @@ -11,7 +11,6 @@ jobs: steps: - checkout: self - submodules: true - task: Gradle@3 inputs: diff --git a/submodules/durabletask-protobuf b/submodules/durabletask-protobuf deleted file mode 160000 index 443b333f..00000000 --- a/submodules/durabletask-protobuf +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 443b333f4f65a438dc9eb4f090560d232afec4b7 From e025538d06254ee975f6b3ed38d46734b13b9e57 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 23 Jan 2025 12:03:43 -0800 Subject: [PATCH 09/53] update --- CHANGELOG.md | 1 + client/build.gradle | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f7c68d11..6d73d90f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ ## placeholder +* Add automatic proto file download and commit hash tracking during build ([#TBD](https://github.com/microsoft/durabletask-java/pull/TBD)) * Fix infinite loop when use continueasnew after wait external event ([#183](https://github.com/microsoft/durabletask-java/pull/183)) * Fix the issue "Deserialize Exception got swallowed when use anyOf with external event." ([#185](https://github.com/microsoft/durabletask-java/pull/185)) diff --git a/client/build.gradle b/client/build.gradle index e18c2561..abdbd409 100644 --- a/client/build.gradle +++ b/client/build.gradle @@ -51,6 +51,27 @@ compileTestJava { options.forkOptions.executable = "${PATH_TO_TEST_JAVA_RUNTIME}/bin/javac" } +task downloadProtoFiles { + doLast { + def protoDir = file("${rootProject.projectDir}/internal/durabletask-protobuf/protos") + protoDir.mkdirs() + + // Download the proto file + new URL('https://raw.githubusercontent.com/microsoft/durabletask-protobuf/main/protos/orchestrator_service.proto') + .withInputStream { i -> + new File(protoDir, 'orchestrator_service.proto').withOutputStream { it << i } + } + + // Get and save the commit hash + def commitHashFile = new File("${rootProject.projectDir}/internal/durabletask-protobuf/PROTO_SOURCE_COMMIT_HASH") + def commitApiUrl = new URL('https://api.github.com/repos/microsoft/durabletask-protobuf/commits?path=protos/orchestrator_service.proto&sha=main&per_page=1') + def connection = commitApiUrl.openConnection() + connection.setRequestProperty('Accept', 'application/vnd.github.v3+json') + def commitHash = new groovy.json.JsonSlurper().parse(connection.inputStream)[0].sha + commitHashFile.text = commitHash + } +} + protobuf { protoc { artifact = "com.google.protobuf:protoc:${protocVersion}" } plugins { @@ -58,6 +79,7 @@ protobuf { } generateProtoTasks { all()*.plugins { grpc {} } + all()*.dependsOn downloadProtoFiles } } From 6ea3a65f57f71c6c629cf8dddcff441b347cd48e Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 23 Jan 2025 12:21:38 -0800 Subject: [PATCH 10/53] update --- internal/durabletask-protobuf/LICENSE | 21 + .../PROTO_SOURCE_COMMIT_HASH | 1 + internal/durabletask-protobuf/README.md | 52 ++ .../lib/java/.gitattributes | 6 + .../durabletask-protobuf/lib/java/.gitignore | 5 + .../lib/java/build.gradle | 52 ++ .../java/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59203 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 + .../durabletask-protobuf/lib/java/gradlew | 185 ++++++ .../durabletask-protobuf/lib/java/gradlew.bat | 89 +++ .../lib/java/settings.gradle | 10 + .../protos/orchestrator_service.proto | 623 ++++++++++++++++++ 12 files changed, 1049 insertions(+) create mode 100644 internal/durabletask-protobuf/LICENSE create mode 100644 internal/durabletask-protobuf/PROTO_SOURCE_COMMIT_HASH create mode 100644 internal/durabletask-protobuf/README.md create mode 100644 internal/durabletask-protobuf/lib/java/.gitattributes create mode 100644 internal/durabletask-protobuf/lib/java/.gitignore create mode 100644 internal/durabletask-protobuf/lib/java/build.gradle create mode 100644 internal/durabletask-protobuf/lib/java/gradle/wrapper/gradle-wrapper.jar create mode 100644 internal/durabletask-protobuf/lib/java/gradle/wrapper/gradle-wrapper.properties create mode 100644 internal/durabletask-protobuf/lib/java/gradlew create mode 100644 internal/durabletask-protobuf/lib/java/gradlew.bat create mode 100644 internal/durabletask-protobuf/lib/java/settings.gradle create mode 100644 internal/durabletask-protobuf/protos/orchestrator_service.proto diff --git a/internal/durabletask-protobuf/LICENSE b/internal/durabletask-protobuf/LICENSE new file mode 100644 index 00000000..9e841e7a --- /dev/null +++ b/internal/durabletask-protobuf/LICENSE @@ -0,0 +1,21 @@ + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE diff --git a/internal/durabletask-protobuf/PROTO_SOURCE_COMMIT_HASH b/internal/durabletask-protobuf/PROTO_SOURCE_COMMIT_HASH new file mode 100644 index 00000000..9544b5da --- /dev/null +++ b/internal/durabletask-protobuf/PROTO_SOURCE_COMMIT_HASH @@ -0,0 +1 @@ +443b333f4f65a438dc9eb4f090560d232afec4b7 \ No newline at end of file diff --git a/internal/durabletask-protobuf/README.md b/internal/durabletask-protobuf/README.md new file mode 100644 index 00000000..7d080c99 --- /dev/null +++ b/internal/durabletask-protobuf/README.md @@ -0,0 +1,52 @@ +# Durable Task Protobuf Files + +This directory contains the protocol buffer definitions used by the Durable Task Framework Java SDK. The files in this directory are automatically downloaded and updated during the build process from the [microsoft/durabletask-protobuf](https://github.com/microsoft/durabletask-protobuf) repository. + +## Directory Structure + +- `protos/` - Contains the downloaded proto files +- `PROTO_SOURCE_COMMIT_HASH` - Contains the commit hash of the latest proto file version + +## Auto-Update Process + +The proto files are automatically downloaded and updated when running Gradle builds. This is handled by the `downloadProtoFiles` task in the `client/build.gradle` file. The task: + +1. Downloads the latest version of `orchestrator_service.proto` +2. Saves the current commit hash for tracking purposes +3. Updates these files before proto compilation begins + +## Manual Update + +If you need to manually update the proto files, you can run: + +```bash +./gradlew downloadProtoFiles +``` + +# Durable Task Protobuf Definitions + +This repo contains [protocol buffer](https://developers.google.com/protocol-buffers) (protobuf) definitions +used by the Durable Task framework sidecar architecture. It's recommended that Durable Task language SDKs reference +the protobuf contracts in this repo via [Git submodules](https://git-scm.com/book/en/v2/Git-Tools-Submodules). + +## Contributing + +This project welcomes contributions and suggestions. Most contributions require you to agree to a +Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us +the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. + +When you submit a pull request, a CLA bot will automatically determine whether you need to provide +a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions +provided by the bot. You will only need to do this once across all repos using our CLA. + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). +For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or +contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. + +## Trademarks + +This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft +trademarks or logos is subject to and must follow +[Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/legal/intellectualproperty/trademarks/usage/general). +Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. +Any use of third-party trademarks or logos are subject to those third-party's policies. diff --git a/internal/durabletask-protobuf/lib/java/.gitattributes b/internal/durabletask-protobuf/lib/java/.gitattributes new file mode 100644 index 00000000..00a51aff --- /dev/null +++ b/internal/durabletask-protobuf/lib/java/.gitattributes @@ -0,0 +1,6 @@ +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# These are explicitly windows files and should use crlf +*.bat text eol=crlf + diff --git a/internal/durabletask-protobuf/lib/java/.gitignore b/internal/durabletask-protobuf/lib/java/.gitignore new file mode 100644 index 00000000..1b6985c0 --- /dev/null +++ b/internal/durabletask-protobuf/lib/java/.gitignore @@ -0,0 +1,5 @@ +# Ignore Gradle project-specific cache directory +.gradle + +# Ignore Gradle build output directory +build diff --git a/internal/durabletask-protobuf/lib/java/build.gradle b/internal/durabletask-protobuf/lib/java/build.gradle new file mode 100644 index 00000000..6b41a390 --- /dev/null +++ b/internal/durabletask-protobuf/lib/java/build.gradle @@ -0,0 +1,52 @@ +plugins { + id 'com.google.protobuf' version '0.8.16' + id 'java-library' +} + +repositories { + mavenCentral() +} + +version = '0.1.0' +archivesBaseName = 'durabletask-grpc' + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +def grpcVersion = '1.46.0' +def protobufVersion = '3.12.0' +def protocVersion = protobufVersion + +dependencies { + implementation "io.grpc:grpc-protobuf:${grpcVersion}" + implementation "io.grpc:grpc-stub:${grpcVersion}" + compileOnly "org.apache.tomcat:annotations-api:6.0.53" + + runtimeOnly "io.grpc:grpc-netty-shaded:${grpcVersion}" + + testImplementation "io.grpc:grpc-testing:${grpcVersion}" + testImplementation "junit:junit:4.12" + testImplementation "org.mockito:mockito-core:3.4.0" +} + +protobuf { + protoc { artifact = "com.google.protobuf:protoc:${protocVersion}" } + plugins { + grpc { artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}" } + } + generateProtoTasks { + all()*.plugins { grpc {} } + } +} + +sourceSets { + main { + proto { + srcDir '../../protos' + } + java { + srcDirs 'build/generated/source/proto/main/grpc' + srcDirs 'build/generated/source/proto/main/java' + } + } +} diff --git a/internal/durabletask-protobuf/lib/java/gradle/wrapper/gradle-wrapper.jar b/internal/durabletask-protobuf/lib/java/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..e708b1c023ec8b20f512888fe07c5bd3ff77bb8f GIT binary patch literal 59203 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

K7UpUK$>l@ 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 E2O(~oYXATM literal 0 HcmV?d00001 diff --git a/internal/durabletask-protobuf/lib/java/gradle/wrapper/gradle-wrapper.properties b/internal/durabletask-protobuf/lib/java/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..0f80bbf5 --- /dev/null +++ b/internal/durabletask-protobuf/lib/java/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/internal/durabletask-protobuf/lib/java/gradlew b/internal/durabletask-protobuf/lib/java/gradlew new file mode 100644 index 00000000..4f906e0c --- /dev/null +++ b/internal/durabletask-protobuf/lib/java/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or 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 +# +# https://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. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/internal/durabletask-protobuf/lib/java/gradlew.bat b/internal/durabletask-protobuf/lib/java/gradlew.bat new file mode 100644 index 00000000..107acd32 --- /dev/null +++ b/internal/durabletask-protobuf/lib/java/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/internal/durabletask-protobuf/lib/java/settings.gradle b/internal/durabletask-protobuf/lib/java/settings.gradle new file mode 100644 index 00000000..88caa5c2 --- /dev/null +++ b/internal/durabletask-protobuf/lib/java/settings.gradle @@ -0,0 +1,10 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * The settings file is used to specify which projects to include in your build. + * + * Detailed information about configuring a multi-project build in Gradle can be found + * in the user manual at https://docs.gradle.org/7.0.2/userguide/multi_project_builds.html + */ + +rootProject.name = 'durabletask' diff --git a/internal/durabletask-protobuf/protos/orchestrator_service.proto b/internal/durabletask-protobuf/protos/orchestrator_service.proto new file mode 100644 index 00000000..64d5cf65 --- /dev/null +++ b/internal/durabletask-protobuf/protos/orchestrator_service.proto @@ -0,0 +1,623 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +syntax = "proto3"; + +option csharp_namespace = "Microsoft.DurableTask.Protobuf"; +option java_package = "com.microsoft.durabletask.implementation.protobuf"; +option go_package = "/internal/protos"; + +import "google/protobuf/timestamp.proto"; +import "google/protobuf/duration.proto"; +import "google/protobuf/wrappers.proto"; +import "google/protobuf/empty.proto"; + +message OrchestrationInstance { + string instanceId = 1; + google.protobuf.StringValue executionId = 2; +} + +message ActivityRequest { + string name = 1; + google.protobuf.StringValue version = 2; + google.protobuf.StringValue input = 3; + OrchestrationInstance orchestrationInstance = 4; + int32 taskId = 5; + TraceContext parentTraceContext = 6; +} + +message ActivityResponse { + string instanceId = 1; + int32 taskId = 2; + google.protobuf.StringValue result = 3; + TaskFailureDetails failureDetails = 4; + string completionToken = 5; +} + +message TaskFailureDetails { + string errorType = 1; + string errorMessage = 2; + google.protobuf.StringValue stackTrace = 3; + TaskFailureDetails innerFailure = 4; + bool isNonRetriable = 5; +} + +enum OrchestrationStatus { + ORCHESTRATION_STATUS_RUNNING = 0; + ORCHESTRATION_STATUS_COMPLETED = 1; + ORCHESTRATION_STATUS_CONTINUED_AS_NEW = 2; + ORCHESTRATION_STATUS_FAILED = 3; + ORCHESTRATION_STATUS_CANCELED = 4; + ORCHESTRATION_STATUS_TERMINATED = 5; + ORCHESTRATION_STATUS_PENDING = 6; + ORCHESTRATION_STATUS_SUSPENDED = 7; +} + +message ParentInstanceInfo { + int32 taskScheduledId = 1; + google.protobuf.StringValue name = 2; + google.protobuf.StringValue version = 3; + OrchestrationInstance orchestrationInstance = 4; +} + +message TraceContext { + string traceParent = 1; + string spanID = 2 [deprecated=true]; + google.protobuf.StringValue traceState = 3; +} + +message ExecutionStartedEvent { + string name = 1; + google.protobuf.StringValue version = 2; + google.protobuf.StringValue input = 3; + OrchestrationInstance orchestrationInstance = 4; + ParentInstanceInfo parentInstance = 5; + google.protobuf.Timestamp scheduledStartTimestamp = 6; + TraceContext parentTraceContext = 7; + google.protobuf.StringValue orchestrationSpanID = 8; +} + +message ExecutionCompletedEvent { + OrchestrationStatus orchestrationStatus = 1; + google.protobuf.StringValue result = 2; + TaskFailureDetails failureDetails = 3; +} + +message ExecutionTerminatedEvent { + google.protobuf.StringValue input = 1; + bool recurse = 2; +} + +message TaskScheduledEvent { + string name = 1; + google.protobuf.StringValue version = 2; + google.protobuf.StringValue input = 3; + TraceContext parentTraceContext = 4; +} + +message TaskCompletedEvent { + int32 taskScheduledId = 1; + google.protobuf.StringValue result = 2; +} + +message TaskFailedEvent { + int32 taskScheduledId = 1; + TaskFailureDetails failureDetails = 2; +} + +message SubOrchestrationInstanceCreatedEvent { + string instanceId = 1; + string name = 2; + google.protobuf.StringValue version = 3; + google.protobuf.StringValue input = 4; + TraceContext parentTraceContext = 5; +} + +message SubOrchestrationInstanceCompletedEvent { + int32 taskScheduledId = 1; + google.protobuf.StringValue result = 2; +} + +message SubOrchestrationInstanceFailedEvent { + int32 taskScheduledId = 1; + TaskFailureDetails failureDetails = 2; +} + +message TimerCreatedEvent { + google.protobuf.Timestamp fireAt = 1; +} + +message TimerFiredEvent { + google.protobuf.Timestamp fireAt = 1; + int32 timerId = 2; +} + +message OrchestratorStartedEvent { + // No payload data +} + +message OrchestratorCompletedEvent { + // No payload data +} + +message EventSentEvent { + string instanceId = 1; + string name = 2; + google.protobuf.StringValue input = 3; +} + +message EventRaisedEvent { + string name = 1; + google.protobuf.StringValue input = 2; +} + +message GenericEvent { + google.protobuf.StringValue data = 1; +} + +message HistoryStateEvent { + OrchestrationState orchestrationState = 1; +} + +message ContinueAsNewEvent { + google.protobuf.StringValue input = 1; +} + +message ExecutionSuspendedEvent { + google.protobuf.StringValue input = 1; +} + +message ExecutionResumedEvent { + google.protobuf.StringValue input = 1; +} + +message HistoryEvent { + int32 eventId = 1; + google.protobuf.Timestamp timestamp = 2; + oneof eventType { + ExecutionStartedEvent executionStarted = 3; + ExecutionCompletedEvent executionCompleted = 4; + ExecutionTerminatedEvent executionTerminated = 5; + TaskScheduledEvent taskScheduled = 6; + TaskCompletedEvent taskCompleted = 7; + TaskFailedEvent taskFailed = 8; + SubOrchestrationInstanceCreatedEvent subOrchestrationInstanceCreated = 9; + SubOrchestrationInstanceCompletedEvent subOrchestrationInstanceCompleted = 10; + SubOrchestrationInstanceFailedEvent subOrchestrationInstanceFailed = 11; + TimerCreatedEvent timerCreated = 12; + TimerFiredEvent timerFired = 13; + OrchestratorStartedEvent orchestratorStarted = 14; + OrchestratorCompletedEvent orchestratorCompleted = 15; + EventSentEvent eventSent = 16; + EventRaisedEvent eventRaised = 17; + GenericEvent genericEvent = 18; + HistoryStateEvent historyState = 19; + ContinueAsNewEvent continueAsNew = 20; + ExecutionSuspendedEvent executionSuspended = 21; + ExecutionResumedEvent executionResumed = 22; + } +} + +message ScheduleTaskAction { + string name = 1; + google.protobuf.StringValue version = 2; + google.protobuf.StringValue input = 3; +} + +message CreateSubOrchestrationAction { + string instanceId = 1; + string name = 2; + google.protobuf.StringValue version = 3; + google.protobuf.StringValue input = 4; +} + +message CreateTimerAction { + google.protobuf.Timestamp fireAt = 1; +} + +message SendEventAction { + OrchestrationInstance instance = 1; + string name = 2; + google.protobuf.StringValue data = 3; +} + +message CompleteOrchestrationAction { + OrchestrationStatus orchestrationStatus = 1; + google.protobuf.StringValue result = 2; + google.protobuf.StringValue details = 3; + google.protobuf.StringValue newVersion = 4; + repeated HistoryEvent carryoverEvents = 5; + TaskFailureDetails failureDetails = 6; +} + +message TerminateOrchestrationAction { + string instanceId = 1; + google.protobuf.StringValue reason = 2; + bool recurse = 3; +} + +message OrchestratorAction { + int32 id = 1; + oneof orchestratorActionType { + ScheduleTaskAction scheduleTask = 2; + CreateSubOrchestrationAction createSubOrchestration = 3; + CreateTimerAction createTimer = 4; + SendEventAction sendEvent = 5; + CompleteOrchestrationAction completeOrchestration = 6; + TerminateOrchestrationAction terminateOrchestration = 7; + } +} + +message OrchestratorRequest { + string instanceId = 1; + google.protobuf.StringValue executionId = 2; + repeated HistoryEvent pastEvents = 3; + repeated HistoryEvent newEvents = 4; + OrchestratorEntityParameters entityParameters = 5; +} + +message OrchestratorResponse { + string instanceId = 1; + repeated OrchestratorAction actions = 2; + google.protobuf.StringValue customStatus = 3; + string completionToken = 4; +} + +message CreateInstanceRequest { + string instanceId = 1; + string name = 2; + google.protobuf.StringValue version = 3; + google.protobuf.StringValue input = 4; + google.protobuf.Timestamp scheduledStartTimestamp = 5; + OrchestrationIdReusePolicy orchestrationIdReusePolicy = 6; + google.protobuf.StringValue executionId = 7; + map tags = 8; +} + +message OrchestrationIdReusePolicy { + repeated OrchestrationStatus operationStatus = 1; + CreateOrchestrationAction action = 2; +} + +enum CreateOrchestrationAction { + ERROR = 0; + IGNORE = 1; + TERMINATE = 2; +} + +message CreateInstanceResponse { + string instanceId = 1; +} + +message GetInstanceRequest { + string instanceId = 1; + bool getInputsAndOutputs = 2; +} + +message GetInstanceResponse { + bool exists = 1; + OrchestrationState orchestrationState = 2; +} + +message RewindInstanceRequest { + string instanceId = 1; + google.protobuf.StringValue reason = 2; +} + +message RewindInstanceResponse { + // Empty for now. Using explicit type incase we want to add content later. +} + +message OrchestrationState { + string instanceId = 1; + string name = 2; + google.protobuf.StringValue version = 3; + OrchestrationStatus orchestrationStatus = 4; + google.protobuf.Timestamp scheduledStartTimestamp = 5; + google.protobuf.Timestamp createdTimestamp = 6; + google.protobuf.Timestamp lastUpdatedTimestamp = 7; + google.protobuf.StringValue input = 8; + google.protobuf.StringValue output = 9; + google.protobuf.StringValue customStatus = 10; + TaskFailureDetails failureDetails = 11; + google.protobuf.StringValue executionId = 12; + google.protobuf.Timestamp completedTimestamp = 13; + google.protobuf.StringValue parentInstanceId = 14; +} + +message RaiseEventRequest { + string instanceId = 1; + string name = 2; + google.protobuf.StringValue input = 3; +} + +message RaiseEventResponse { + // No payload +} + +message TerminateRequest { + string instanceId = 1; + google.protobuf.StringValue output = 2; + bool recursive = 3; +} + +message TerminateResponse { + // No payload +} + +message SuspendRequest { + string instanceId = 1; + google.protobuf.StringValue reason = 2; +} + +message SuspendResponse { + // No payload +} + +message ResumeRequest { + string instanceId = 1; + google.protobuf.StringValue reason = 2; +} + +message ResumeResponse { + // No payload +} + +message QueryInstancesRequest { + InstanceQuery query = 1; +} + +message InstanceQuery{ + repeated OrchestrationStatus runtimeStatus = 1; + google.protobuf.Timestamp createdTimeFrom = 2; + google.protobuf.Timestamp createdTimeTo = 3; + repeated google.protobuf.StringValue taskHubNames = 4; + int32 maxInstanceCount = 5; + google.protobuf.StringValue continuationToken = 6; + google.protobuf.StringValue instanceIdPrefix = 7; + bool fetchInputsAndOutputs = 8; +} + +message QueryInstancesResponse { + repeated OrchestrationState orchestrationState = 1; + google.protobuf.StringValue continuationToken = 2; +} + +message PurgeInstancesRequest { + oneof request { + string instanceId = 1; + PurgeInstanceFilter purgeInstanceFilter = 2; + } + bool recursive = 3; +} + +message PurgeInstanceFilter { + google.protobuf.Timestamp createdTimeFrom = 1; + google.protobuf.Timestamp createdTimeTo = 2; + repeated OrchestrationStatus runtimeStatus = 3; +} + +message PurgeInstancesResponse { + int32 deletedInstanceCount = 1; +} + +message CreateTaskHubRequest { + bool recreateIfExists = 1; +} + +message CreateTaskHubResponse { + //no playload +} + +message DeleteTaskHubRequest { + //no playload +} + +message DeleteTaskHubResponse { + //no playload +} + +message SignalEntityRequest { + string instanceId = 1; + string name = 2; + google.protobuf.StringValue input = 3; + string requestId = 4; + google.protobuf.Timestamp scheduledTime = 5; +} + +message SignalEntityResponse { + // no payload +} + +message GetEntityRequest { + string instanceId = 1; + bool includeState = 2; +} + +message GetEntityResponse { + bool exists = 1; + EntityMetadata entity = 2; +} + +message EntityQuery { + google.protobuf.StringValue instanceIdStartsWith = 1; + google.protobuf.Timestamp lastModifiedFrom = 2; + google.protobuf.Timestamp lastModifiedTo = 3; + bool includeState = 4; + bool includeTransient = 5; + google.protobuf.Int32Value pageSize = 6; + google.protobuf.StringValue continuationToken = 7; +} + +message QueryEntitiesRequest { + EntityQuery query = 1; +} + +message QueryEntitiesResponse { + repeated EntityMetadata entities = 1; + google.protobuf.StringValue continuationToken = 2; +} + +message EntityMetadata { + string instanceId = 1; + google.protobuf.Timestamp lastModifiedTime = 2; + int32 backlogQueueSize = 3; + google.protobuf.StringValue lockedBy = 4; + google.protobuf.StringValue serializedState = 5; +} + +message CleanEntityStorageRequest { + google.protobuf.StringValue continuationToken = 1; + bool removeEmptyEntities = 2; + bool releaseOrphanedLocks = 3; +} + +message CleanEntityStorageResponse { + google.protobuf.StringValue continuationToken = 1; + int32 emptyEntitiesRemoved = 2; + int32 orphanedLocksReleased = 3; +} + +message OrchestratorEntityParameters { + google.protobuf.Duration entityMessageReorderWindow = 1; +} + +message EntityBatchRequest { + string instanceId = 1; + google.protobuf.StringValue entityState = 2; + repeated OperationRequest operations = 3; +} + +message EntityBatchResult { + repeated OperationResult results = 1; + repeated OperationAction actions = 2; + google.protobuf.StringValue entityState = 3; + TaskFailureDetails failureDetails = 4; +} + +message OperationRequest { + string operation = 1; + string requestId = 2; + google.protobuf.StringValue input = 3; +} + +message OperationResult { + oneof resultType { + OperationResultSuccess success = 1; + OperationResultFailure failure = 2; + } +} + +message OperationResultSuccess { + google.protobuf.StringValue result = 1; +} + +message OperationResultFailure { + TaskFailureDetails failureDetails = 1; +} + +message OperationAction { + int32 id = 1; + oneof operationActionType { + SendSignalAction sendSignal = 2; + StartNewOrchestrationAction startNewOrchestration = 3; + } +} + +message SendSignalAction { + string instanceId = 1; + string name = 2; + google.protobuf.StringValue input = 3; + google.protobuf.Timestamp scheduledTime = 4; +} + +message StartNewOrchestrationAction { + string instanceId = 1; + string name = 2; + google.protobuf.StringValue version = 3; + google.protobuf.StringValue input = 4; + google.protobuf.Timestamp scheduledTime = 5; +} + +service TaskHubSidecarService { + // Sends a hello request to the sidecar service. + rpc Hello(google.protobuf.Empty) returns (google.protobuf.Empty); + + // Starts a new orchestration instance. + rpc StartInstance(CreateInstanceRequest) returns (CreateInstanceResponse); + + // Gets the status of an existing orchestration instance. + rpc GetInstance(GetInstanceRequest) returns (GetInstanceResponse); + + // Rewinds an orchestration instance to last known good state and replays from there. + rpc RewindInstance(RewindInstanceRequest) returns (RewindInstanceResponse); + + // Waits for an orchestration instance to reach a running or completion state. + rpc WaitForInstanceStart(GetInstanceRequest) returns (GetInstanceResponse); + + // Waits for an orchestration instance to reach a completion state (completed, failed, terminated, etc.). + rpc WaitForInstanceCompletion(GetInstanceRequest) returns (GetInstanceResponse); + + // Raises an event to a running orchestration instance. + rpc RaiseEvent(RaiseEventRequest) returns (RaiseEventResponse); + + // Terminates a running orchestration instance. + rpc TerminateInstance(TerminateRequest) returns (TerminateResponse); + + // Suspends a running orchestration instance. + rpc SuspendInstance(SuspendRequest) returns (SuspendResponse); + + // Resumes a suspended orchestration instance. + rpc ResumeInstance(ResumeRequest) returns (ResumeResponse); + + // rpc DeleteInstance(DeleteInstanceRequest) returns (DeleteInstanceResponse); + + rpc QueryInstances(QueryInstancesRequest) returns (QueryInstancesResponse); + rpc PurgeInstances(PurgeInstancesRequest) returns (PurgeInstancesResponse); + + rpc GetWorkItems(GetWorkItemsRequest) returns (stream WorkItem); + rpc CompleteActivityTask(ActivityResponse) returns (CompleteTaskResponse); + rpc CompleteOrchestratorTask(OrchestratorResponse) returns (CompleteTaskResponse); + rpc CompleteEntityTask(EntityBatchResult) returns (CompleteTaskResponse); + + // Deletes and Creates the necessary resources for the orchestration service and the instance store + rpc CreateTaskHub(CreateTaskHubRequest) returns (CreateTaskHubResponse); + + // Deletes the resources for the orchestration service and optionally the instance store + rpc DeleteTaskHub(DeleteTaskHubRequest) returns (DeleteTaskHubResponse); + + // sends a signal to an entity + rpc SignalEntity(SignalEntityRequest) returns (SignalEntityResponse); + + // get information about a specific entity + rpc GetEntity(GetEntityRequest) returns (GetEntityResponse); + + // query entities + rpc QueryEntities(QueryEntitiesRequest) returns (QueryEntitiesResponse); + + // clean entity storage + rpc CleanEntityStorage(CleanEntityStorageRequest) returns (CleanEntityStorageResponse); +} + +message GetWorkItemsRequest { + int32 maxConcurrentOrchestrationWorkItems = 1; + int32 maxConcurrentActivityWorkItems = 2; +} + +message WorkItem { + oneof request { + OrchestratorRequest orchestratorRequest = 1; + ActivityRequest activityRequest = 2; + EntityBatchRequest entityRequest = 3; + HealthPing healthPing = 4; + } + string completionToken = 10; +} + +message CompleteTaskResponse { + // No payload +} + +message HealthPing { + // No payload +} \ No newline at end of file From 2270ed2a037655798d3cd3bf5aa2ed62930fbf2d Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 23 Jan 2025 13:16:04 -0800 Subject: [PATCH 11/53] signing is disabled, should not exclude as there is no sign --- .github/workflows/build-validation.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-validation.yml b/.github/workflows/build-validation.yml index 9e675001..45f52a19 100644 --- a/.github/workflows/build-validation.yml +++ b/.github/workflows/build-validation.yml @@ -85,7 +85,7 @@ jobs: uses: gradle/gradle-build-action@v2 - name: Publish to local - run: ./gradlew publishToMavenLocal -x sign + run: ./gradlew publishToMavenLocal - name: Build azure functions sample run: ./gradlew azureFunctionsPackage @@ -124,7 +124,7 @@ jobs: uses: gradle/gradle-build-action@v2 - name: Publish to local - run: ./gradlew publishToMavenLocal -x sign + run: ./gradlew publishToMavenLocal - name: Build azure functions sample run: ./gradlew azureFunctionsPackage From 9a02b23b3b9c3f863b0435323551b16c8f7f61a9 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 29 Jan 2025 00:03:11 -0800 Subject: [PATCH 12/53] save --- .../durabletask/AccessTokenCache.java | 3 +++ internal/durabletask-protobuf/LICENSE | 21 ------------------- 2 files changed, 3 insertions(+), 21 deletions(-) delete mode 100644 internal/durabletask-protobuf/LICENSE diff --git a/client/src/main/java/com/microsoft/durabletask/AccessTokenCache.java b/client/src/main/java/com/microsoft/durabletask/AccessTokenCache.java index bf6cad7c..f4928ab8 100644 --- a/client/src/main/java/com/microsoft/durabletask/AccessTokenCache.java +++ b/client/src/main/java/com/microsoft/durabletask/AccessTokenCache.java @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package com.microsoft.durabletask; import com.azure.core.credential.TokenCredential; diff --git a/internal/durabletask-protobuf/LICENSE b/internal/durabletask-protobuf/LICENSE deleted file mode 100644 index 9e841e7a..00000000 --- a/internal/durabletask-protobuf/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ - MIT License - - Copyright (c) Microsoft Corporation. - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE From 8fb193f15cd112cd8c6a007568bda73108b5aaaf Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 29 Jan 2025 00:35:13 -0800 Subject: [PATCH 13/53] feedback changes --- CHANGELOG.md | 2 +- client/build.gradle | 8 ++- .../PROTO_SOURCE_COMMIT_HASH | 2 +- internal/durabletask-protobuf/README.md | 32 +--------- .../protos/orchestrator_service.proto | 63 ++++++++++++++++++- 5 files changed, 71 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d73d90f..038a2ce9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ ## placeholder -* Add automatic proto file download and commit hash tracking during build ([#TBD](https://github.com/microsoft/durabletask-java/pull/TBD)) +* Add automatic proto file download and commit hash tracking during build ([#201](https://github.com/microsoft/durabletask-java/pull/201)) * Fix infinite loop when use continueasnew after wait external event ([#183](https://github.com/microsoft/durabletask-java/pull/183)) * Fix the issue "Deserialize Exception got swallowed when use anyOf with external event." ([#185](https://github.com/microsoft/durabletask-java/pull/185)) diff --git a/client/build.gradle b/client/build.gradle index abdbd409..3f712e47 100644 --- a/client/build.gradle +++ b/client/build.gradle @@ -17,7 +17,7 @@ def jacksonVersion = '2.15.3' // When build on local, you need to set this value to your local jdk11 directory. // Java11 is used to compile and run all the tests. // Example for Windows: C:/Program Files/Java/openjdk-11.0.12_7/ -def PATH_TO_TEST_JAVA_RUNTIME = "$System.env.JDK_11" +def PATH_TO_TEST_JAVA_RUNTIME = System.env.JDK_11 ?: System.getProperty("java.home") dependencies { @@ -52,19 +52,21 @@ compileTestJava { } task downloadProtoFiles { + ext.branch = project.hasProperty('protoBranch') ? project.protoBranch : 'main' + doLast { def protoDir = file("${rootProject.projectDir}/internal/durabletask-protobuf/protos") protoDir.mkdirs() // Download the proto file - new URL('https://raw.githubusercontent.com/microsoft/durabletask-protobuf/main/protos/orchestrator_service.proto') + new URL("https://raw.githubusercontent.com/microsoft/durabletask-protobuf/${ext.branch}/protos/orchestrator_service.proto") .withInputStream { i -> new File(protoDir, 'orchestrator_service.proto').withOutputStream { it << i } } // Get and save the commit hash def commitHashFile = new File("${rootProject.projectDir}/internal/durabletask-protobuf/PROTO_SOURCE_COMMIT_HASH") - def commitApiUrl = new URL('https://api.github.com/repos/microsoft/durabletask-protobuf/commits?path=protos/orchestrator_service.proto&sha=main&per_page=1') + def commitApiUrl = new URL("https://api.github.com/repos/microsoft/durabletask-protobuf/commits?path=protos/orchestrator_service.proto&sha=${ext.branch}&per_page=1") def connection = commitApiUrl.openConnection() connection.setRequestProperty('Accept', 'application/vnd.github.v3+json') def commitHash = new groovy.json.JsonSlurper().parse(connection.inputStream)[0].sha diff --git a/internal/durabletask-protobuf/PROTO_SOURCE_COMMIT_HASH b/internal/durabletask-protobuf/PROTO_SOURCE_COMMIT_HASH index 9544b5da..1f80142c 100644 --- a/internal/durabletask-protobuf/PROTO_SOURCE_COMMIT_HASH +++ b/internal/durabletask-protobuf/PROTO_SOURCE_COMMIT_HASH @@ -1 +1 @@ -443b333f4f65a438dc9eb4f090560d232afec4b7 \ No newline at end of file +310000510e1c1544a4e99172007bd058ade66c55 \ No newline at end of file diff --git a/internal/durabletask-protobuf/README.md b/internal/durabletask-protobuf/README.md index 7d080c99..6b310469 100644 --- a/internal/durabletask-protobuf/README.md +++ b/internal/durabletask-protobuf/README.md @@ -20,33 +20,5 @@ The proto files are automatically downloaded and updated when running Gradle bui If you need to manually update the proto files, you can run: ```bash -./gradlew downloadProtoFiles -``` - -# Durable Task Protobuf Definitions - -This repo contains [protocol buffer](https://developers.google.com/protocol-buffers) (protobuf) definitions -used by the Durable Task framework sidecar architecture. It's recommended that Durable Task language SDKs reference -the protobuf contracts in this repo via [Git submodules](https://git-scm.com/book/en/v2/Git-Tools-Submodules). - -## Contributing - -This project welcomes contributions and suggestions. Most contributions require you to agree to a -Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us -the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. - -When you submit a pull request, a CLA bot will automatically determine whether you need to provide -a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions -provided by the bot. You will only need to do this once across all repos using our CLA. - -This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). -For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or -contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. - -## Trademarks - -This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft -trademarks or logos is subject to and must follow -[Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/legal/intellectualproperty/trademarks/usage/general). -Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. -Any use of third-party trademarks or logos are subject to those third-party's policies. +./gradlew downloadProtoFiles -PprotoBranch= +``` \ No newline at end of file diff --git a/internal/durabletask-protobuf/protos/orchestrator_service.proto b/internal/durabletask-protobuf/protos/orchestrator_service.proto index 64d5cf65..24a59438 100644 --- a/internal/durabletask-protobuf/protos/orchestrator_service.proto +++ b/internal/durabletask-protobuf/protos/orchestrator_service.proto @@ -171,6 +171,51 @@ message ExecutionResumedEvent { google.protobuf.StringValue input = 1; } +message EntityOperationSignaledEvent { + string requestId = 1; + string operation = 2; + google.protobuf.Timestamp scheduledTime = 3; + google.protobuf.StringValue input = 4; + google.protobuf.StringValue targetInstanceId = 5; // used only within histories, null in messages +} + +message EntityOperationCalledEvent { + string requestId = 1; + string operation = 2; + google.protobuf.Timestamp scheduledTime = 3; + google.protobuf.StringValue input = 4; + google.protobuf.StringValue parentInstanceId = 5; // used only within messages, null in histories + google.protobuf.StringValue parentExecutionId = 6; // used only within messages, null in histories + google.protobuf.StringValue targetInstanceId = 7; // used only within histories, null in messages +} + +message EntityLockRequestedEvent { + string criticalSectionId = 1; + repeated string lockSet = 2; + int32 position = 3; + google.protobuf.StringValue parentInstanceId = 4; // used only within messages, null in histories +} + +message EntityOperationCompletedEvent { + string requestId = 1; + google.protobuf.StringValue output = 2; +} + +message EntityOperationFailedEvent { + string requestId = 1; + TaskFailureDetails failureDetails = 2; +} + +message EntityUnlockSentEvent { + string criticalSectionId = 1; + google.protobuf.StringValue parentInstanceId = 2; // used only within messages, null in histories + google.protobuf.StringValue targetInstanceId = 3; // used only within histories, null in messages +} + +message EntityLockGrantedEvent { + string criticalSectionId = 1; +} + message HistoryEvent { int32 eventId = 1; google.protobuf.Timestamp timestamp = 2; @@ -195,6 +240,13 @@ message HistoryEvent { ContinueAsNewEvent continueAsNew = 20; ExecutionSuspendedEvent executionSuspended = 21; ExecutionResumedEvent executionResumed = 22; + EntityOperationSignaledEvent entityOperationSignaled = 23; + EntityOperationCalledEvent entityOperationCalled = 24; + EntityOperationCompletedEvent entityOperationCompleted = 25; + EntityOperationFailedEvent entityOperationFailed = 26; + EntityLockRequestedEvent entityLockRequested = 27; + EntityLockGrantedEvent entityLockGranted = 28; + EntityUnlockSentEvent entityUnlockSent = 29; } } @@ -495,6 +547,13 @@ message EntityBatchResult { TaskFailureDetails failureDetails = 4; } +message EntityRequest { + string instanceId = 1; + string executionId = 2; + google.protobuf.StringValue entityState = 3; // null if entity does not exist + repeated HistoryEvent operationRequests = 4; +} + message OperationRequest { string operation = 1; string requestId = 2; @@ -602,14 +661,16 @@ service TaskHubSidecarService { message GetWorkItemsRequest { int32 maxConcurrentOrchestrationWorkItems = 1; int32 maxConcurrentActivityWorkItems = 2; + int32 maxConcurrentEntityWorkItems = 3; } message WorkItem { oneof request { OrchestratorRequest orchestratorRequest = 1; ActivityRequest activityRequest = 2; - EntityBatchRequest entityRequest = 3; + EntityBatchRequest entityRequest = 3; // (older) used by orchestration services implementations HealthPing healthPing = 4; + EntityRequest entityRequestV2 = 5; // (newer) used by backend service implementations } string completionToken = 10; } From a51ba86d6b54222ae9aba0f0520c36fe473eb327 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 29 Jan 2025 01:01:12 -0800 Subject: [PATCH 14/53] delete legacy lib/java --- .../lib/java/.gitattributes | 6 - .../durabletask-protobuf/lib/java/.gitignore | 5 - .../lib/java/build.gradle | 52 ----- .../java/gradle/wrapper/gradle-wrapper.jar | Bin 59203 -> 0 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 - .../durabletask-protobuf/lib/java/gradlew | 185 ------------------ .../durabletask-protobuf/lib/java/gradlew.bat | 89 --------- .../lib/java/settings.gradle | 10 - 8 files changed, 352 deletions(-) delete mode 100644 internal/durabletask-protobuf/lib/java/.gitattributes delete mode 100644 internal/durabletask-protobuf/lib/java/.gitignore delete mode 100644 internal/durabletask-protobuf/lib/java/build.gradle delete mode 100644 internal/durabletask-protobuf/lib/java/gradle/wrapper/gradle-wrapper.jar delete mode 100644 internal/durabletask-protobuf/lib/java/gradle/wrapper/gradle-wrapper.properties delete mode 100644 internal/durabletask-protobuf/lib/java/gradlew delete mode 100644 internal/durabletask-protobuf/lib/java/gradlew.bat delete mode 100644 internal/durabletask-protobuf/lib/java/settings.gradle diff --git a/internal/durabletask-protobuf/lib/java/.gitattributes b/internal/durabletask-protobuf/lib/java/.gitattributes deleted file mode 100644 index 00a51aff..00000000 --- a/internal/durabletask-protobuf/lib/java/.gitattributes +++ /dev/null @@ -1,6 +0,0 @@ -# -# https://help.github.com/articles/dealing-with-line-endings/ -# -# These are explicitly windows files and should use crlf -*.bat text eol=crlf - diff --git a/internal/durabletask-protobuf/lib/java/.gitignore b/internal/durabletask-protobuf/lib/java/.gitignore deleted file mode 100644 index 1b6985c0..00000000 --- a/internal/durabletask-protobuf/lib/java/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -# Ignore Gradle project-specific cache directory -.gradle - -# Ignore Gradle build output directory -build diff --git a/internal/durabletask-protobuf/lib/java/build.gradle b/internal/durabletask-protobuf/lib/java/build.gradle deleted file mode 100644 index 6b41a390..00000000 --- a/internal/durabletask-protobuf/lib/java/build.gradle +++ /dev/null @@ -1,52 +0,0 @@ -plugins { - id 'com.google.protobuf' version '0.8.16' - id 'java-library' -} - -repositories { - mavenCentral() -} - -version = '0.1.0' -archivesBaseName = 'durabletask-grpc' - -sourceCompatibility = 1.8 -targetCompatibility = 1.8 - -def grpcVersion = '1.46.0' -def protobufVersion = '3.12.0' -def protocVersion = protobufVersion - -dependencies { - implementation "io.grpc:grpc-protobuf:${grpcVersion}" - implementation "io.grpc:grpc-stub:${grpcVersion}" - compileOnly "org.apache.tomcat:annotations-api:6.0.53" - - runtimeOnly "io.grpc:grpc-netty-shaded:${grpcVersion}" - - testImplementation "io.grpc:grpc-testing:${grpcVersion}" - testImplementation "junit:junit:4.12" - testImplementation "org.mockito:mockito-core:3.4.0" -} - -protobuf { - protoc { artifact = "com.google.protobuf:protoc:${protocVersion}" } - plugins { - grpc { artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}" } - } - generateProtoTasks { - all()*.plugins { grpc {} } - } -} - -sourceSets { - main { - proto { - srcDir '../../protos' - } - java { - srcDirs 'build/generated/source/proto/main/grpc' - srcDirs 'build/generated/source/proto/main/java' - } - } -} diff --git a/internal/durabletask-protobuf/lib/java/gradle/wrapper/gradle-wrapper.jar b/internal/durabletask-protobuf/lib/java/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index e708b1c023ec8b20f512888fe07c5bd3ff77bb8f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 59203 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

K7UpUK$>l@ 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 E2O(~oYXATM diff --git a/internal/durabletask-protobuf/lib/java/gradle/wrapper/gradle-wrapper.properties b/internal/durabletask-protobuf/lib/java/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 0f80bbf5..00000000 --- a/internal/durabletask-protobuf/lib/java/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/internal/durabletask-protobuf/lib/java/gradlew b/internal/durabletask-protobuf/lib/java/gradlew deleted file mode 100644 index 4f906e0c..00000000 --- a/internal/durabletask-protobuf/lib/java/gradlew +++ /dev/null @@ -1,185 +0,0 @@ -#!/usr/bin/env sh - -# -# Copyright 2015 the original author or 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 -# -# https://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. -# - -############################################################################## -## -## Gradle start up script for UN*X -## -############################################################################## - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn () { - echo "$*" -} - -die () { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=`expr $i + 1` - done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` - -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" - -exec "$JAVACMD" "$@" diff --git a/internal/durabletask-protobuf/lib/java/gradlew.bat b/internal/durabletask-protobuf/lib/java/gradlew.bat deleted file mode 100644 index 107acd32..00000000 --- a/internal/durabletask-protobuf/lib/java/gradlew.bat +++ /dev/null @@ -1,89 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/internal/durabletask-protobuf/lib/java/settings.gradle b/internal/durabletask-protobuf/lib/java/settings.gradle deleted file mode 100644 index 88caa5c2..00000000 --- a/internal/durabletask-protobuf/lib/java/settings.gradle +++ /dev/null @@ -1,10 +0,0 @@ -/* - * This file was generated by the Gradle 'init' task. - * - * The settings file is used to specify which projects to include in your build. - * - * Detailed information about configuring a multi-project build in Gradle can be found - * in the user manual at https://docs.gradle.org/7.0.2/userguide/multi_project_builds.html - */ - -rootProject.name = 'durabletask' From 7241e34aba7808ba75a7aa813c6dc11d302a5c21 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 29 Jan 2025 01:16:03 -0800 Subject: [PATCH 15/53] create skeleton project for pkg "com.microsoft.durabletask-azuremanaged" --- azuremanaged/build.gradle | 79 +++++++++++++++++++ .../azuremanaged}/AccessTokenCache.java | 4 +- client/build.gradle | 2 - settings.gradle | 3 +- 4 files changed, 83 insertions(+), 5 deletions(-) create mode 100644 azuremanaged/build.gradle rename {client/src/main/java/com/microsoft/durabletask => azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged}/AccessTokenCache.java (95%) diff --git a/azuremanaged/build.gradle b/azuremanaged/build.gradle new file mode 100644 index 00000000..7c661a86 --- /dev/null +++ b/azuremanaged/build.gradle @@ -0,0 +1,79 @@ +plugins { + id 'java-library' + id 'maven-publish' + id 'signing' +} + +group 'com.microsoft' +version = '1.5.0' +archivesBaseName = 'durabletask-azuremanaged' + +def jacksonVersion = '2.15.3' + +dependencies { + // TODO: update the package then dependency here + // api project(':client') + implementation 'com.azure:azure-identity:1.15.0' + + // Jackson dependencies if needed by the Azure package + implementation "com.fasterxml.jackson.core:jackson-core:${jacksonVersion}" + implementation "com.fasterxml.jackson.core:jackson-databind:${jacksonVersion}" + implementation "com.fasterxml.jackson.core:jackson-annotations:${jacksonVersion}" + implementation "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:${jacksonVersion}" + + testImplementation(platform('org.junit:junit-bom:5.7.2')) + testImplementation('org.junit.jupiter:junit-jupiter') +} + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + withSourcesJar() + withJavadocJar() +} + +test { + useJUnitPlatform() +} + +publishing { + repositories { + maven { + url "file://$project.rootDir/repo" + } + } + publications { + mavenJava(MavenPublication) { + from components.java + artifactId = archivesBaseName + pom { + name = 'Durable Task Azure Managed SDK for Java' + description = 'This package contains Azure-specific extensions for the Durable Task Framework in Java.' + url = "https://github.com/microsoft/durabletask-java/tree/main/azuremanaged" + licenses { + license { + name = "MIT License" + url = "https://opensource.org/licenses/MIT" + distribution = "repo" + } + } + developers { + developer { + id = "Microsoft" + name = "Microsoft Corporation" + } + } + scm { + connection = "scm:git:https://github.com/microsoft/durabletask-java" + developerConnection = "scm:git:git@github.com:microsoft/durabletask-java" + url = "https://github.com/microsoft/durabletask-java/tree/main/azuremanaged" + } + } + } + } +} + +java { + withSourcesJar() + withJavadocJar() +} \ No newline at end of file diff --git a/client/src/main/java/com/microsoft/durabletask/AccessTokenCache.java b/azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/AccessTokenCache.java similarity index 95% rename from client/src/main/java/com/microsoft/durabletask/AccessTokenCache.java rename to azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/AccessTokenCache.java index f4928ab8..7f697771 100644 --- a/client/src/main/java/com/microsoft/durabletask/AccessTokenCache.java +++ b/azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/AccessTokenCache.java @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.microsoft.durabletask; +package com.microsoft.durabletask.azuremanaged; import com.azure.core.credential.TokenCredential; import com.azure.core.credential.AccessToken; @@ -32,4 +32,4 @@ public AccessToken getToken() { return cachedToken; } -} \ No newline at end of file +} \ No newline at end of file diff --git a/client/build.gradle b/client/build.gradle index 3f712e47..d080a052 100644 --- a/client/build.gradle +++ b/client/build.gradle @@ -34,8 +34,6 @@ dependencies { implementation "com.fasterxml.jackson.core:jackson-annotations:${jacksonVersion}" implementation "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:${jacksonVersion}" - implementation 'com.azure:azure-identity:1.15.0' - testImplementation(platform('org.junit:junit-bom:5.7.2')) testImplementation('org.junit.jupiter:junit-jupiter') } diff --git a/settings.gradle b/settings.gradle index b0f523f0..5e2f3b65 100644 --- a/settings.gradle +++ b/settings.gradle @@ -4,5 +4,6 @@ include ":client" include ":azurefunctions" include ":samples" include ":samples-azure-functions" -include 'endtoendtests' +include ":endtoendtests" +include ":azuremanaged" From 86883203ab1566d0c2d3e812478bfb7492c42be7 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 14 Mar 2025 12:27:12 -0700 Subject: [PATCH 16/53] split pkg in progress --- azuremanaged/build.gradle | 79 -------- client/build.gradle | 6 + .../DurableTaskSchedulerClientExtensions.java | 103 ++++++++++ .../DurableTaskSchedulerClientOptions.java | 176 ++++++++++++++++++ .../azuremanaged/AccessTokenCache.java | 18 +- .../DurableTaskSchedulerConnectionString.java | 122 ++++++++++++ .../DurableTaskSchedulerWorkerExtensions.java | 102 ++++++++++ .../DurableTaskSchedulerWorkerOptions.java | 176 ++++++++++++++++++ .../PROTO_SOURCE_COMMIT_HASH | 2 +- .../protos/orchestrator_service.proto | 64 ++++++- .../WebAppToDurableTaskSchedulerSample.java | 116 ++++-------- 11 files changed, 792 insertions(+), 172 deletions(-) delete mode 100644 azuremanaged/build.gradle create mode 100644 client/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientExtensions.java create mode 100644 client/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientOptions.java rename {azuremanaged/src/main/java/com/microsoft/durabletask => client/src/main/java/com/microsoft/durabletask/shared}/azuremanaged/AccessTokenCache.java (61%) create mode 100644 client/src/main/java/com/microsoft/durabletask/shared/azuremanaged/DurableTaskSchedulerConnectionString.java create mode 100644 client/src/main/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerExtensions.java create mode 100644 client/src/main/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerOptions.java diff --git a/azuremanaged/build.gradle b/azuremanaged/build.gradle deleted file mode 100644 index 7c661a86..00000000 --- a/azuremanaged/build.gradle +++ /dev/null @@ -1,79 +0,0 @@ -plugins { - id 'java-library' - id 'maven-publish' - id 'signing' -} - -group 'com.microsoft' -version = '1.5.0' -archivesBaseName = 'durabletask-azuremanaged' - -def jacksonVersion = '2.15.3' - -dependencies { - // TODO: update the package then dependency here - // api project(':client') - implementation 'com.azure:azure-identity:1.15.0' - - // Jackson dependencies if needed by the Azure package - implementation "com.fasterxml.jackson.core:jackson-core:${jacksonVersion}" - implementation "com.fasterxml.jackson.core:jackson-databind:${jacksonVersion}" - implementation "com.fasterxml.jackson.core:jackson-annotations:${jacksonVersion}" - implementation "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:${jacksonVersion}" - - testImplementation(platform('org.junit:junit-bom:5.7.2')) - testImplementation('org.junit.jupiter:junit-jupiter') -} - -java { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - withSourcesJar() - withJavadocJar() -} - -test { - useJUnitPlatform() -} - -publishing { - repositories { - maven { - url "file://$project.rootDir/repo" - } - } - publications { - mavenJava(MavenPublication) { - from components.java - artifactId = archivesBaseName - pom { - name = 'Durable Task Azure Managed SDK for Java' - description = 'This package contains Azure-specific extensions for the Durable Task Framework in Java.' - url = "https://github.com/microsoft/durabletask-java/tree/main/azuremanaged" - licenses { - license { - name = "MIT License" - url = "https://opensource.org/licenses/MIT" - distribution = "repo" - } - } - developers { - developer { - id = "Microsoft" - name = "Microsoft Corporation" - } - } - scm { - connection = "scm:git:https://github.com/microsoft/durabletask-java" - developerConnection = "scm:git:git@github.com:microsoft/durabletask-java" - url = "https://github.com/microsoft/durabletask-java/tree/main/azuremanaged" - } - } - } - } -} - -java { - withSourcesJar() - withJavadocJar() -} \ No newline at end of file diff --git a/client/build.gradle b/client/build.gradle index d080a052..277ec5ec 100644 --- a/client/build.gradle +++ b/client/build.gradle @@ -14,6 +14,8 @@ archivesBaseName = 'durabletask-client' def grpcVersion = '1.59.0' def protocVersion = '3.12.0' def jacksonVersion = '2.15.3' +def azureCoreVersion = '1.45.0' +def azureIdentityVersion = '1.11.1' // When build on local, you need to set this value to your local jdk11 directory. // Java11 is used to compile and run all the tests. // Example for Windows: C:/Program Files/Java/openjdk-11.0.12_7/ @@ -33,6 +35,10 @@ dependencies { implementation "com.fasterxml.jackson.core:jackson-databind:${jacksonVersion}" implementation "com.fasterxml.jackson.core:jackson-annotations:${jacksonVersion}" implementation "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:${jacksonVersion}" + + // Azure dependencies for authentication + implementation "com.azure:azure-core:${azureCoreVersion}" + implementation "com.azure:azure-identity:${azureIdentityVersion}" testImplementation(platform('org.junit:junit-bom:5.7.2')) testImplementation('org.junit.jupiter:junit-jupiter') diff --git a/client/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientExtensions.java b/client/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientExtensions.java new file mode 100644 index 00000000..b3c62571 --- /dev/null +++ b/client/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientExtensions.java @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.durabletask.client.azuremanaged; + +import com.azure.core.credential.TokenCredential; +import com.azure.core.credential.TokenRequestContext; +import com.microsoft.durabletask.DurableTaskClient; +import com.microsoft.durabletask.DurableTaskGrpcClientBuilder; +import com.microsoft.durabletask.shared.azuremanaged.AccessTokenCache; + +import io.grpc.Channel; +import io.grpc.ClientCall; +import io.grpc.ClientInterceptor; +import io.grpc.ForwardingClientCall; +import io.grpc.ManagedChannelBuilder; +import io.grpc.Metadata; +import io.grpc.MethodDescriptor; +import io.grpc.CallOptions; + +import java.util.Objects; + +/** + * Extension methods for creating DurableTaskClient instances that connect to Azure-managed Durable Task Scheduler. + */ +public class DurableTaskSchedulerClientExtensions { + + /** + * Creates a DurableTaskClient that connects to Azure-managed Durable Task Scheduler. + * + * @param options The options for connecting to Azure-managed Durable Task Scheduler. + * @return A new DurableTaskClient instance. + */ + public static DurableTaskClient createClient(DurableTaskSchedulerClientOptions options) { + Objects.requireNonNull(options, "options must not be null"); + options.validate(); + + // Create the access token cache if credentials are provided + AccessTokenCache tokenCache = null; + TokenCredential credential = options.getTokenCredential(); + if (credential != null) { + TokenRequestContext context = new TokenRequestContext(); + context.addScopes(new String[] { options.getResourceId() + "/.default" }); + tokenCache = new AccessTokenCache(credential, context, options.getTokenRefreshMargin()); + } + + // Create the gRPC channel + Channel grpcChannel = createGrpcChannel(options, tokenCache); + + // Create and return the client + return new DurableTaskGrpcClientBuilder() + .grpcChannel(grpcChannel) + .build(); + } + + private static Channel createGrpcChannel(DurableTaskSchedulerClientOptions options, AccessTokenCache tokenCache) { + // Normalize the endpoint URL and add DNS scheme for gRPC name resolution + String endpoint = "dns:///" + options.getEndpoint(); + + // Create metadata interceptor to add task hub name and auth token + ClientInterceptor metadataInterceptor = new ClientInterceptor() { + @Override + public ClientCall interceptCall( + MethodDescriptor method, + CallOptions callOptions, + Channel next) { + return new ForwardingClientCall.SimpleForwardingClientCall( + next.newCall(method, callOptions)) { + @Override + public void start(ClientCall.Listener responseListener, Metadata headers) { + headers.put( + Metadata.Key.of("taskhub", Metadata.ASCII_STRING_MARSHALLER), + options.getTaskHubName() + ); + + // Add authorization token if credentials are configured + if (tokenCache != null) { + String token = tokenCache.getToken().getToken(); + headers.put( + Metadata.Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER), + "Bearer " + token + ); + } + + super.start(responseListener, headers); + } + }; + } + }; + + // Build the channel with appropriate security settings + ManagedChannelBuilder builder = ManagedChannelBuilder.forTarget(endpoint) + .intercept(metadataInterceptor); + + if (!options.isAllowInsecure()) { + builder.useTransportSecurity(); + } else { + builder.usePlaintext(); + } + + return builder.build(); + } +} \ No newline at end of file diff --git a/client/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientOptions.java b/client/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientOptions.java new file mode 100644 index 00000000..ff0a99ca --- /dev/null +++ b/client/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientOptions.java @@ -0,0 +1,176 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.durabletask.client.azuremanaged; + +import com.azure.core.credential.TokenCredential; +import com.microsoft.durabletask.shared.azuremanaged.DurableTaskSchedulerConnectionString; + +import java.time.Duration; +import java.util.Objects; + +/** + * Configuration options for connecting to Azure-managed Durable Task Scheduler as a client. + */ +public class DurableTaskSchedulerClientOptions { + private String endpoint; + private String taskHubName; + private String resourceId = "https://durabletask.io"; + private boolean allowInsecure = false; + private TokenCredential tokenCredential; + private Duration tokenRefreshMargin = Duration.ofMinutes(5); + + /** + * Creates a new instance of DurableTaskSchedulerClientOptions. + */ + public DurableTaskSchedulerClientOptions() { + } + + /** + * Creates a new instance of DurableTaskSchedulerClientOptions from a connection string. + * + * @param connectionString The connection string to parse. + * @return A new DurableTaskSchedulerClientOptions object. + */ + public static DurableTaskSchedulerClientOptions fromConnectionString(String connectionString) { + DurableTaskSchedulerConnectionString parsedConnectionString = DurableTaskSchedulerConnectionString.parse(connectionString); + + DurableTaskSchedulerClientOptions options = new DurableTaskSchedulerClientOptions(); + options.setEndpoint(parsedConnectionString.getEndpoint()); + options.setTaskHubName(parsedConnectionString.getTaskHubName()); + options.setResourceId(parsedConnectionString.getResourceId()); + options.setAllowInsecure(parsedConnectionString.isAllowInsecure()); + + return options; + } + + /** + * Gets the endpoint URL. + * + * @return The endpoint URL. + */ + public String getEndpoint() { + return endpoint; + } + + /** + * Sets the endpoint URL. + * + * @param endpoint The endpoint URL. + * @return This options object. + */ + public DurableTaskSchedulerClientOptions setEndpoint(String endpoint) { + this.endpoint = endpoint; + return this; + } + + /** + * Gets the task hub name. + * + * @return The task hub name. + */ + public String getTaskHubName() { + return taskHubName; + } + + /** + * Sets the task hub name. + * + * @param taskHubName The task hub name. + * @return This options object. + */ + public DurableTaskSchedulerClientOptions setTaskHubName(String taskHubName) { + this.taskHubName = taskHubName; + return this; + } + + /** + * Gets the resource ID for authentication. + * + * @return The resource ID. + */ + public String getResourceId() { + return resourceId; + } + + /** + * Sets the resource ID for authentication. + * + * @param resourceId The resource ID. + * @return This options object. + */ + public DurableTaskSchedulerClientOptions setResourceId(String resourceId) { + this.resourceId = resourceId; + return this; + } + + /** + * Gets whether insecure connections are allowed. + * + * @return True if insecure connections are allowed, false otherwise. + */ + public boolean isAllowInsecure() { + return allowInsecure; + } + + /** + * Sets whether insecure connections are allowed. + * + * @param allowInsecure True to allow insecure connections, false otherwise. + * @return This options object. + */ + public DurableTaskSchedulerClientOptions setAllowInsecure(boolean allowInsecure) { + this.allowInsecure = allowInsecure; + return this; + } + + /** + * Gets the token credential for authentication. + * + * @return The token credential. + */ + public TokenCredential getTokenCredential() { + return tokenCredential; + } + + /** + * Sets the token credential for authentication. + * + * @param tokenCredential The token credential. + * @return This options object. + */ + public DurableTaskSchedulerClientOptions setTokenCredential(TokenCredential tokenCredential) { + this.tokenCredential = tokenCredential; + return this; + } + + /** + * Gets the token refresh margin. + * + * @return The token refresh margin. + */ + public Duration getTokenRefreshMargin() { + return tokenRefreshMargin; + } + + /** + * Sets the token refresh margin. + * + * @param tokenRefreshMargin The token refresh margin. + * @return This options object. + */ + public DurableTaskSchedulerClientOptions setTokenRefreshMargin(Duration tokenRefreshMargin) { + this.tokenRefreshMargin = tokenRefreshMargin; + return this; + } + + /** + * Validates that the options are properly configured. + * + * @throws IllegalArgumentException If the options are not properly configured. + */ + public void validate() { + Objects.requireNonNull(endpoint, "endpoint must not be null"); + Objects.requireNonNull(taskHubName, "taskHubName must not be null"); + } +} \ No newline at end of file diff --git a/azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/AccessTokenCache.java b/client/src/main/java/com/microsoft/durabletask/shared/azuremanaged/AccessTokenCache.java similarity index 61% rename from azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/AccessTokenCache.java rename to client/src/main/java/com/microsoft/durabletask/shared/azuremanaged/AccessTokenCache.java index 7f697771..28027593 100644 --- a/azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/AccessTokenCache.java +++ b/client/src/main/java/com/microsoft/durabletask/shared/azuremanaged/AccessTokenCache.java @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.microsoft.durabletask.azuremanaged; +package com.microsoft.durabletask.shared.azuremanaged; import com.azure.core.credential.TokenCredential; import com.azure.core.credential.AccessToken; @@ -10,18 +10,34 @@ import java.time.Duration; import java.time.OffsetDateTime; +/** + * Caches access tokens for Azure authentication. + * This class is used by both client and worker components to authenticate with Azure-managed Durable Task Scheduler. + */ public final class AccessTokenCache { private final TokenCredential credential; private final TokenRequestContext context; private final Duration margin; private AccessToken cachedToken; + /** + * Creates a new instance of the AccessTokenCache. + * + * @param credential The token credential to use for obtaining tokens. + * @param context The token request context specifying the scopes. + * @param margin The time margin before token expiration to refresh the token. + */ public AccessTokenCache(TokenCredential credential, TokenRequestContext context, Duration margin) { this.credential = credential; this.context = context; this.margin = margin; } + /** + * Gets a valid access token, refreshing it if necessary. + * + * @return A valid access token. + */ public AccessToken getToken() { OffsetDateTime nowWithMargin = OffsetDateTime.now().plus(margin); diff --git a/client/src/main/java/com/microsoft/durabletask/shared/azuremanaged/DurableTaskSchedulerConnectionString.java b/client/src/main/java/com/microsoft/durabletask/shared/azuremanaged/DurableTaskSchedulerConnectionString.java new file mode 100644 index 00000000..4236468f --- /dev/null +++ b/client/src/main/java/com/microsoft/durabletask/shared/azuremanaged/DurableTaskSchedulerConnectionString.java @@ -0,0 +1,122 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.durabletask.shared.azuremanaged; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +/** + * Parses and manages connection strings for Azure-managed Durable Task Scheduler. + * This class is used by both client and worker components. + */ +public class DurableTaskSchedulerConnectionString { + private final String endpoint; + private final String taskHubName; + private final String resourceId; + private final boolean allowInsecure; + + /** + * Creates a new instance of DurableTaskSchedulerConnectionString. + * + * @param endpoint The endpoint URL of the Durable Task Scheduler service. + * @param taskHubName The name of the task hub. + * @param resourceId The Azure resource ID for authentication. + * @param allowInsecure Whether to allow insecure connections. + */ + public DurableTaskSchedulerConnectionString( + String endpoint, + String taskHubName, + String resourceId, + boolean allowInsecure) { + this.endpoint = Objects.requireNonNull(endpoint, "endpoint must not be null"); + this.taskHubName = Objects.requireNonNull(taskHubName, "taskHubName must not be null"); + this.resourceId = resourceId != null ? resourceId : "https://durabletask.io"; + this.allowInsecure = allowInsecure; + } + + /** + * Parses a connection string into a DurableTaskSchedulerConnectionString object. + * + * @param connectionString The connection string to parse. + * @return A new DurableTaskSchedulerConnectionString object. + * @throws IllegalArgumentException If the connection string is invalid. + */ + public static DurableTaskSchedulerConnectionString parse(String connectionString) { + if (connectionString == null || connectionString.isEmpty()) { + throw new IllegalArgumentException("connectionString must not be null or empty"); + } + + Map properties = parseConnectionString(connectionString); + + String endpoint = properties.get("Endpoint"); + if (endpoint == null || endpoint.isEmpty()) { + throw new IllegalArgumentException("The connection string must contain an Endpoint property"); + } + + String taskHubName = properties.get("TaskHubName"); + if (taskHubName == null || taskHubName.isEmpty()) { + throw new IllegalArgumentException("The connection string must contain a TaskHubName property"); + } + + String resourceId = properties.get("ResourceId"); + + String allowInsecureStr = properties.get("AllowInsecure"); + boolean allowInsecure = "true".equalsIgnoreCase(allowInsecureStr); + + return new DurableTaskSchedulerConnectionString(endpoint, taskHubName, resourceId, allowInsecure); + } + + private static Map parseConnectionString(String connectionString) { + Map properties = new HashMap<>(); + + String[] pairs = connectionString.split(";"); + for (String pair : pairs) { + int equalsIndex = pair.indexOf('='); + if (equalsIndex > 0) { + String key = pair.substring(0, equalsIndex).trim(); + String value = pair.substring(equalsIndex + 1).trim(); + properties.put(key, value); + } + } + + return properties; + } + + /** + * Gets the endpoint URL. + * + * @return The endpoint URL. + */ + public String getEndpoint() { + return endpoint; + } + + /** + * Gets the task hub name. + * + * @return The task hub name. + */ + public String getTaskHubName() { + return taskHubName; + } + + /** + * Gets the resource ID for authentication. + * + * @return The resource ID. + */ + public String getResourceId() { + return resourceId; + } + + /** + * Gets whether insecure connections are allowed. + * + * @return True if insecure connections are allowed, false otherwise. + */ + public boolean isAllowInsecure() { + return allowInsecure; + } +} \ No newline at end of file diff --git a/client/src/main/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerExtensions.java b/client/src/main/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerExtensions.java new file mode 100644 index 00000000..990706e7 --- /dev/null +++ b/client/src/main/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerExtensions.java @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.durabletask.worker.azuremanaged; + +import com.azure.core.credential.TokenCredential; +import com.azure.core.credential.TokenRequestContext; +import com.microsoft.durabletask.DurableTaskGrpcWorker; +import com.microsoft.durabletask.DurableTaskGrpcWorkerBuilder; +import com.microsoft.durabletask.shared.azuremanaged.AccessTokenCache; + +import io.grpc.Channel; +import io.grpc.ClientCall; +import io.grpc.ClientInterceptor; +import io.grpc.ForwardingClientCall; +import io.grpc.ManagedChannelBuilder; +import io.grpc.Metadata; +import io.grpc.MethodDescriptor; +import io.grpc.CallOptions; + +import java.util.Objects; + +/** + * Extension methods for creating DurableTaskGrpcWorker instances that connect to Azure-managed Durable Task Scheduler. + */ +public class DurableTaskSchedulerWorkerExtensions { + + /** + * Creates a DurableTaskGrpcWorkerBuilder that connects to Azure-managed Durable Task Scheduler. + * + * @param options The options for connecting to Azure-managed Durable Task Scheduler. + * @return A new DurableTaskGrpcWorkerBuilder instance. + */ + public static DurableTaskGrpcWorkerBuilder createWorkerBuilder(DurableTaskSchedulerWorkerOptions options) { + Objects.requireNonNull(options, "options must not be null"); + options.validate(); + + // Create the access token cache if credentials are provided + AccessTokenCache tokenCache = null; + TokenCredential credential = options.getTokenCredential(); + if (credential != null) { + TokenRequestContext context = new TokenRequestContext(); + context.addScopes(new String[] { options.getResourceId() + "/.default" }); + tokenCache = new AccessTokenCache(credential, context, options.getTokenRefreshMargin()); + } + + // Create the gRPC channel + Channel grpcChannel = createGrpcChannel(options, tokenCache); + + // Create and return the worker builder + return new DurableTaskGrpcWorkerBuilder() + .grpcChannel(grpcChannel); + } + + private static Channel createGrpcChannel(DurableTaskSchedulerWorkerOptions options, AccessTokenCache tokenCache) { + // Normalize the endpoint URL and add DNS scheme for gRPC name resolution + String endpoint = "dns:///" + options.getEndpoint(); + + // Create metadata interceptor to add task hub name and auth token + ClientInterceptor metadataInterceptor = new ClientInterceptor() { + @Override + public ClientCall interceptCall( + MethodDescriptor method, + CallOptions callOptions, + Channel next) { + return new ForwardingClientCall.SimpleForwardingClientCall( + next.newCall(method, callOptions)) { + @Override + public void start(ClientCall.Listener responseListener, Metadata headers) { + headers.put( + Metadata.Key.of("taskhub", Metadata.ASCII_STRING_MARSHALLER), + options.getTaskHubName() + ); + + // Add authorization token if credentials are configured + if (tokenCache != null) { + String token = tokenCache.getToken().getToken(); + headers.put( + Metadata.Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER), + "Bearer " + token + ); + } + + super.start(responseListener, headers); + } + }; + } + }; + + // Build the channel with appropriate security settings + ManagedChannelBuilder builder = ManagedChannelBuilder.forTarget(endpoint) + .intercept(metadataInterceptor); + + if (!options.isAllowInsecure()) { + builder.useTransportSecurity(); + } else { + builder.usePlaintext(); + } + + return builder.build(); + } +} \ No newline at end of file diff --git a/client/src/main/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerOptions.java b/client/src/main/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerOptions.java new file mode 100644 index 00000000..43f09360 --- /dev/null +++ b/client/src/main/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerOptions.java @@ -0,0 +1,176 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.durabletask.worker.azuremanaged; + +import com.azure.core.credential.TokenCredential; +import com.microsoft.durabletask.shared.azuremanaged.DurableTaskSchedulerConnectionString; + +import java.time.Duration; +import java.util.Objects; + +/** + * Configuration options for connecting to Azure-managed Durable Task Scheduler as a worker. + */ +public class DurableTaskSchedulerWorkerOptions { + private String endpoint; + private String taskHubName; + private String resourceId = "https://durabletask.io"; + private boolean allowInsecure = false; + private TokenCredential tokenCredential; + private Duration tokenRefreshMargin = Duration.ofMinutes(5); + + /** + * Creates a new instance of DurableTaskSchedulerWorkerOptions. + */ + public DurableTaskSchedulerWorkerOptions() { + } + + /** + * Creates a new instance of DurableTaskSchedulerWorkerOptions from a connection string. + * + * @param connectionString The connection string to parse. + * @return A new DurableTaskSchedulerWorkerOptions object. + */ + public static DurableTaskSchedulerWorkerOptions fromConnectionString(String connectionString) { + DurableTaskSchedulerConnectionString parsedConnectionString = DurableTaskSchedulerConnectionString.parse(connectionString); + + DurableTaskSchedulerWorkerOptions options = new DurableTaskSchedulerWorkerOptions(); + options.setEndpoint(parsedConnectionString.getEndpoint()); + options.setTaskHubName(parsedConnectionString.getTaskHubName()); + options.setResourceId(parsedConnectionString.getResourceId()); + options.setAllowInsecure(parsedConnectionString.isAllowInsecure()); + + return options; + } + + /** + * Gets the endpoint URL. + * + * @return The endpoint URL. + */ + public String getEndpoint() { + return endpoint; + } + + /** + * Sets the endpoint URL. + * + * @param endpoint The endpoint URL. + * @return This options object. + */ + public DurableTaskSchedulerWorkerOptions setEndpoint(String endpoint) { + this.endpoint = endpoint; + return this; + } + + /** + * Gets the task hub name. + * + * @return The task hub name. + */ + public String getTaskHubName() { + return taskHubName; + } + + /** + * Sets the task hub name. + * + * @param taskHubName The task hub name. + * @return This options object. + */ + public DurableTaskSchedulerWorkerOptions setTaskHubName(String taskHubName) { + this.taskHubName = taskHubName; + return this; + } + + /** + * Gets the resource ID for authentication. + * + * @return The resource ID. + */ + public String getResourceId() { + return resourceId; + } + + /** + * Sets the resource ID for authentication. + * + * @param resourceId The resource ID. + * @return This options object. + */ + public DurableTaskSchedulerWorkerOptions setResourceId(String resourceId) { + this.resourceId = resourceId; + return this; + } + + /** + * Gets whether insecure connections are allowed. + * + * @return True if insecure connections are allowed, false otherwise. + */ + public boolean isAllowInsecure() { + return allowInsecure; + } + + /** + * Sets whether insecure connections are allowed. + * + * @param allowInsecure True to allow insecure connections, false otherwise. + * @return This options object. + */ + public DurableTaskSchedulerWorkerOptions setAllowInsecure(boolean allowInsecure) { + this.allowInsecure = allowInsecure; + return this; + } + + /** + * Gets the token credential for authentication. + * + * @return The token credential. + */ + public TokenCredential getTokenCredential() { + return tokenCredential; + } + + /** + * Sets the token credential for authentication. + * + * @param tokenCredential The token credential. + * @return This options object. + */ + public DurableTaskSchedulerWorkerOptions setTokenCredential(TokenCredential tokenCredential) { + this.tokenCredential = tokenCredential; + return this; + } + + /** + * Gets the token refresh margin. + * + * @return The token refresh margin. + */ + public Duration getTokenRefreshMargin() { + return tokenRefreshMargin; + } + + /** + * Sets the token refresh margin. + * + * @param tokenRefreshMargin The token refresh margin. + * @return This options object. + */ + public DurableTaskSchedulerWorkerOptions setTokenRefreshMargin(Duration tokenRefreshMargin) { + this.tokenRefreshMargin = tokenRefreshMargin; + return this; + } + + /** + * Validates that the options are properly configured. + * + * @throws IllegalArgumentException If the options are not properly configured. + */ + public void validate() { + Objects.requireNonNull(endpoint, "endpoint must not be null"); + Objects.requireNonNull(taskHubName, "taskHubName must not be null"); + } +} \ No newline at end of file diff --git a/internal/durabletask-protobuf/PROTO_SOURCE_COMMIT_HASH b/internal/durabletask-protobuf/PROTO_SOURCE_COMMIT_HASH index 1f80142c..e409e9a4 100644 --- a/internal/durabletask-protobuf/PROTO_SOURCE_COMMIT_HASH +++ b/internal/durabletask-protobuf/PROTO_SOURCE_COMMIT_HASH @@ -1 +1 @@ -310000510e1c1544a4e99172007bd058ade66c55 \ No newline at end of file +4792f47019ab2b3e9ea979fb4af72427a4144c51 \ No newline at end of file diff --git a/internal/durabletask-protobuf/protos/orchestrator_service.proto b/internal/durabletask-protobuf/protos/orchestrator_service.proto index 24a59438..64e75281 100644 --- a/internal/durabletask-protobuf/protos/orchestrator_service.proto +++ b/internal/durabletask-protobuf/protos/orchestrator_service.proto @@ -75,6 +75,7 @@ message ExecutionStartedEvent { google.protobuf.Timestamp scheduledStartTimestamp = 6; TraceContext parentTraceContext = 7; google.protobuf.StringValue orchestrationSpanID = 8; + map tags = 9; } message ExecutionCompletedEvent { @@ -288,6 +289,15 @@ message TerminateOrchestrationAction { bool recurse = 3; } +message SendEntityMessageAction { + oneof EntityMessageType { + EntityOperationSignaledEvent entityOperationSignaled = 1; + EntityOperationCalledEvent entityOperationCalled = 2; + EntityLockRequestedEvent entityLockRequested = 3; + EntityUnlockSentEvent entityUnlockSent = 4; + } +} + message OrchestratorAction { int32 id = 1; oneof orchestratorActionType { @@ -297,6 +307,7 @@ message OrchestratorAction { SendEventAction sendEvent = 5; CompleteOrchestrationAction completeOrchestration = 6; TerminateOrchestrationAction terminateOrchestration = 7; + SendEntityMessageAction sendEntityMessage = 8; } } @@ -306,6 +317,7 @@ message OrchestratorRequest { repeated HistoryEvent pastEvents = 3; repeated HistoryEvent newEvents = 4; OrchestratorEntityParameters entityParameters = 5; + bool requiresHistoryStreaming = 6; } message OrchestratorResponse { @@ -313,6 +325,10 @@ message OrchestratorResponse { repeated OrchestratorAction actions = 2; google.protobuf.StringValue customStatus = 3; string completionToken = 4; + + // The number of work item events that were processed by the orchestrator. + // This field is optional. If not set, the service should assume that the orchestrator processed all events. + google.protobuf.Int32Value numEventsProcessed = 5; } message CreateInstanceRequest { @@ -324,17 +340,12 @@ message CreateInstanceRequest { OrchestrationIdReusePolicy orchestrationIdReusePolicy = 6; google.protobuf.StringValue executionId = 7; map tags = 8; + TraceContext parentTraceContext = 9; } message OrchestrationIdReusePolicy { - repeated OrchestrationStatus operationStatus = 1; - CreateOrchestrationAction action = 2; -} - -enum CreateOrchestrationAction { - ERROR = 0; - IGNORE = 1; - TERMINATE = 2; + repeated OrchestrationStatus replaceableStatus = 1; + reserved 2; } message CreateInstanceResponse { @@ -375,6 +386,7 @@ message OrchestrationState { google.protobuf.StringValue executionId = 12; google.protobuf.Timestamp completedTimestamp = 13; google.protobuf.StringValue parentInstanceId = 14; + map tags = 15; } message RaiseEventRequest { @@ -545,6 +557,8 @@ message EntityBatchResult { repeated OperationAction actions = 2; google.protobuf.StringValue entityState = 3; TaskFailureDetails failureDetails = 4; + string completionToken = 5; + repeated OperationInfo operationInfos = 6; // used only with DTS } message EntityRequest { @@ -567,6 +581,11 @@ message OperationResult { } } +message OperationInfo { + string requestId = 1; + OrchestrationInstance responseDestination = 2; // null for signals +} + message OperationResultSuccess { google.protobuf.StringValue result = 1; } @@ -639,6 +658,9 @@ service TaskHubSidecarService { rpc CompleteOrchestratorTask(OrchestratorResponse) returns (CompleteTaskResponse); rpc CompleteEntityTask(EntityBatchResult) returns (CompleteTaskResponse); + // Gets the history of an orchestration instance as a stream of events. + rpc StreamInstanceHistory(StreamInstanceHistoryRequest) returns (stream HistoryChunk); + // Deletes and Creates the necessary resources for the orchestration service and the instance store rpc CreateTaskHub(CreateTaskHubRequest) returns (CreateTaskHubResponse); @@ -662,6 +684,18 @@ message GetWorkItemsRequest { int32 maxConcurrentOrchestrationWorkItems = 1; int32 maxConcurrentActivityWorkItems = 2; int32 maxConcurrentEntityWorkItems = 3; + + repeated WorkerCapability capabilities = 10; +} + +enum WorkerCapability { + WORKER_CAPABILITY_UNSPECIFIED = 0; + + // Indicates that the worker is capable of streaming instance history as a more optimized + // alternative to receiving the full history embedded in the orchestrator work-item. + // When set, the service may return work items without any history events as an optimization. + // It is strongly recommended that all SDKs support this capability. + WORKER_CAPABILITY_HISTORY_STREAMING = 1; } message WorkItem { @@ -681,4 +715,16 @@ message CompleteTaskResponse { message HealthPing { // No payload -} \ No newline at end of file +} + +message StreamInstanceHistoryRequest { + string instanceId = 1; + google.protobuf.StringValue executionId = 2; + + // When set to true, the service may return a more optimized response suitable for workers. + bool forWorkItemProcessing = 3; +} + +message HistoryChunk { + repeated HistoryEvent events = 1; +} diff --git a/samples/src/main/java/io/durabletask/samples/WebAppToDurableTaskSchedulerSample.java b/samples/src/main/java/io/durabletask/samples/WebAppToDurableTaskSchedulerSample.java index 1fe9ee2b..d2c73978 100644 --- a/samples/src/main/java/io/durabletask/samples/WebAppToDurableTaskSchedulerSample.java +++ b/samples/src/main/java/io/durabletask/samples/WebAppToDurableTaskSchedulerSample.java @@ -4,6 +4,12 @@ import com.azure.core.credential.AccessToken; import com.microsoft.durabletask.*; +import com.microsoft.durabletask.client.azuremanaged.DurableTaskSchedulerClientExtensions; +import com.microsoft.durabletask.client.azuremanaged.DurableTaskSchedulerClientOptions; +import com.microsoft.durabletask.worker.azuremanaged.DurableTaskSchedulerWorkerExtensions; +import com.microsoft.durabletask.worker.azuremanaged.DurableTaskSchedulerWorkerOptions; +import com.microsoft.durabletask.shared.azuremanaged.AccessTokenCache; + import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @@ -17,22 +23,11 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.beans.factory.annotation.Value; import java.time.Duration; -import io.grpc.ManagedChannelBuilder; import io.grpc.Channel; -import io.grpc.ClientCall; -import io.grpc.ClientInterceptor; -import io.grpc.ForwardingClientCall; -import io.grpc.Metadata; -import io.grpc.MethodDescriptor; -import io.grpc.CallOptions; -import io.grpc.ChannelCredentials; -import io.grpc.TlsChannelCredentials; -import io.grpc.InsecureChannelCredentials; -import com.azure.core.credential.TokenCredential; -import com.azure.core.credential.TokenRequestContext; import java.util.Objects; import com.azure.identity.DefaultAzureCredentialBuilder; -import reactor.core.publisher.Mono; +import com.azure.core.credential.TokenCredential; +import com.azure.core.credential.TokenRequestContext; @ConfigurationProperties(prefix = "durable.task") @lombok.Data @@ -84,74 +79,21 @@ public AccessTokenCache accessTokenCache( ); } - @Bean(name = "workerChannel") - public Channel workerGrpcChannel( - DurableTaskProperties properties, - AccessTokenCache tokenCache) { - return createChannel(properties, tokenCache); - } - - @Bean(name = "clientChannel") - public Channel clientGrpcChannel( + @Bean(destroyMethod = "stop") + public DurableTaskGrpcWorker durableTaskWorker( DurableTaskProperties properties, - AccessTokenCache tokenCache) { - return createChannel(properties, tokenCache); - } - - private Channel createChannel(DurableTaskProperties properties, AccessTokenCache tokenCache) { - Objects.requireNonNull(properties.getHubName(), "taskHubName must not be null"); + TokenCredential tokenCredential) { - // Normalize the endpoint URL and add DNS scheme for gRPC name resolution - String endpoint = "dns:///" + properties.getEndpoint(); - - // Create metadata interceptor to add task hub name and auth token - ClientInterceptor metadataInterceptor = new ClientInterceptor() { - @Override - public ClientCall interceptCall( - MethodDescriptor method, - CallOptions callOptions, - Channel next) { - return new ForwardingClientCall.SimpleForwardingClientCall( - next.newCall(method, callOptions)) { - @Override - public void start(ClientCall.Listener responseListener, Metadata headers) { - headers.put( - Metadata.Key.of("taskhub", Metadata.ASCII_STRING_MARSHALLER), - properties.getHubName() - ); - - // Add authorization token if credentials are configured - if (tokenCache != null) { - String token = tokenCache.getToken().getToken(); - headers.put( - Metadata.Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER), - "Bearer " + token - ); - } - - super.start(responseListener, headers); - } - }; - } - }; + // Create worker options + DurableTaskSchedulerWorkerOptions options = new DurableTaskSchedulerWorkerOptions() + .setEndpoint(properties.getEndpoint()) + .setTaskHubName(properties.getHubName()) + .setResourceId(properties.getResourceId()) + .setAllowInsecure(properties.isAllowInsecure()) + .setTokenCredential(tokenCredential); - // Build the channel with appropriate security settings - ManagedChannelBuilder builder = ManagedChannelBuilder.forTarget(endpoint) - .intercept(metadataInterceptor); - - if (!properties.isAllowInsecure()) { - builder.useTransportSecurity(); - } else { - builder.usePlaintext(); - } - - return builder.build(); - } - - @Bean(destroyMethod = "stop") - public DurableTaskGrpcWorker durableTaskWorker(@Qualifier("workerChannel") Channel grpcChannel) { - DurableTaskGrpcWorkerBuilder builder = new DurableTaskGrpcWorkerBuilder() - .grpcChannel(grpcChannel); + // Create worker builder + DurableTaskGrpcWorkerBuilder builder = DurableTaskSchedulerWorkerExtensions.createWorkerBuilder(options); // Add orchestrations builder.addOrchestration(new TaskOrchestrationFactory() { @@ -240,10 +182,20 @@ public TaskActivity create() { } @Bean - public DurableTaskClient durableTaskClient(@Qualifier("clientChannel") Channel grpcChannel) { - return new DurableTaskGrpcClientBuilder() - .grpcChannel(grpcChannel) - .build(); + public DurableTaskClient durableTaskClient( + DurableTaskProperties properties, + TokenCredential tokenCredential) { + + // Create client options + DurableTaskSchedulerClientOptions options = new DurableTaskSchedulerClientOptions() + .setEndpoint(properties.getEndpoint()) + .setTaskHubName(properties.getHubName()) + .setResourceId(properties.getResourceId()) + .setAllowInsecure(properties.isAllowInsecure()) + .setTokenCredential(tokenCredential); + + // Create and return the client + return DurableTaskSchedulerClientExtensions.createClient(options); } } From 628c093031a2a754fa49615e1a65e6946d95ab5b Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 18 Mar 2025 08:51:45 -0700 Subject: [PATCH 17/53] move out --- azuremanaged/client/build.gradle | 32 +++++++++++++++++++ azuremanaged/client/settings.gradle | 18 +++++++++++ .../DurableTaskSchedulerClientExtensions.java | 0 .../DurableTaskSchedulerClientOptions.java | 0 .../shared}/AccessTokenCache.java | 0 .../DurableTaskSchedulerConnectionString.java | 0 azuremanaged/worker/build.gradle | 32 +++++++++++++++++++ azuremanaged/worker/settings.gradle | 18 +++++++++++ .../DurableTaskSchedulerWorkerExtensions.java | 0 .../DurableTaskSchedulerWorkerOptions.java | 0 10 files changed, 100 insertions(+) create mode 100644 azuremanaged/client/build.gradle create mode 100644 azuremanaged/client/settings.gradle rename {client => azuremanaged/client}/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientExtensions.java (100%) rename {client => azuremanaged/client}/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientOptions.java (100%) rename {client/src/main/java/com/microsoft/durabletask/shared/azuremanaged => azuremanaged/shared}/AccessTokenCache.java (100%) rename {client/src/main/java/com/microsoft/durabletask/shared/azuremanaged => azuremanaged/shared}/DurableTaskSchedulerConnectionString.java (100%) create mode 100644 azuremanaged/worker/build.gradle create mode 100644 azuremanaged/worker/settings.gradle rename {client => azuremanaged/worker}/src/main/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerExtensions.java (100%) rename {client => azuremanaged/worker}/src/main/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerOptions.java (100%) diff --git a/azuremanaged/client/build.gradle b/azuremanaged/client/build.gradle new file mode 100644 index 00000000..46867078 --- /dev/null +++ b/azuremanaged/client/build.gradle @@ -0,0 +1,32 @@ +/* + * This build file was generated by the Gradle 'init' task. + * + * This generated file contains a sample Java project to get you started. + * For more details take a look at the Java Quickstart chapter in the Gradle + * user guide available at https://docs.gradle.org/4.4.1/userguide/tutorial_java_projects.html + */ + +// Apply the java plugin to add support for Java +apply plugin: 'java' + +// Apply the application plugin to add support for building an application +apply plugin: 'application' + +// In this section you declare where to find the dependencies of your project +repositories { + // Use jcenter for resolving your dependencies. + // You can declare any Maven/Ivy/file repository here. + jcenter() +} + +dependencies { + // This dependency is found on compile classpath of this component and consumers. + compile 'com.google.guava:guava:23.0' + + // Use JUnit test framework + testCompile 'junit:junit:4.12' +} + +// Define the main class for the application +mainClassName = 'App' + diff --git a/azuremanaged/client/settings.gradle b/azuremanaged/client/settings.gradle new file mode 100644 index 00000000..15547c43 --- /dev/null +++ b/azuremanaged/client/settings.gradle @@ -0,0 +1,18 @@ +/* + * This settings file was generated by the Gradle 'init' task. + * + * The settings file is used to specify which projects to include in your build. + * In a single project build this file can be empty or even removed. + * + * Detailed information about configuring a multi-project build in Gradle can be found + * in the user guide at https://docs.gradle.org/4.4.1/userguide/multi_project_builds.html + */ + +/* +// To declare projects as part of a multi-project build use the 'include' method +include 'shared' +include 'api' +include 'services:webservice' +*/ + +rootProject.name = 'client' diff --git a/client/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientExtensions.java b/azuremanaged/client/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientExtensions.java similarity index 100% rename from client/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientExtensions.java rename to azuremanaged/client/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientExtensions.java diff --git a/client/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientOptions.java b/azuremanaged/client/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientOptions.java similarity index 100% rename from client/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientOptions.java rename to azuremanaged/client/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientOptions.java diff --git a/client/src/main/java/com/microsoft/durabletask/shared/azuremanaged/AccessTokenCache.java b/azuremanaged/shared/AccessTokenCache.java similarity index 100% rename from client/src/main/java/com/microsoft/durabletask/shared/azuremanaged/AccessTokenCache.java rename to azuremanaged/shared/AccessTokenCache.java diff --git a/client/src/main/java/com/microsoft/durabletask/shared/azuremanaged/DurableTaskSchedulerConnectionString.java b/azuremanaged/shared/DurableTaskSchedulerConnectionString.java similarity index 100% rename from client/src/main/java/com/microsoft/durabletask/shared/azuremanaged/DurableTaskSchedulerConnectionString.java rename to azuremanaged/shared/DurableTaskSchedulerConnectionString.java diff --git a/azuremanaged/worker/build.gradle b/azuremanaged/worker/build.gradle new file mode 100644 index 00000000..46867078 --- /dev/null +++ b/azuremanaged/worker/build.gradle @@ -0,0 +1,32 @@ +/* + * This build file was generated by the Gradle 'init' task. + * + * This generated file contains a sample Java project to get you started. + * For more details take a look at the Java Quickstart chapter in the Gradle + * user guide available at https://docs.gradle.org/4.4.1/userguide/tutorial_java_projects.html + */ + +// Apply the java plugin to add support for Java +apply plugin: 'java' + +// Apply the application plugin to add support for building an application +apply plugin: 'application' + +// In this section you declare where to find the dependencies of your project +repositories { + // Use jcenter for resolving your dependencies. + // You can declare any Maven/Ivy/file repository here. + jcenter() +} + +dependencies { + // This dependency is found on compile classpath of this component and consumers. + compile 'com.google.guava:guava:23.0' + + // Use JUnit test framework + testCompile 'junit:junit:4.12' +} + +// Define the main class for the application +mainClassName = 'App' + diff --git a/azuremanaged/worker/settings.gradle b/azuremanaged/worker/settings.gradle new file mode 100644 index 00000000..dd76947d --- /dev/null +++ b/azuremanaged/worker/settings.gradle @@ -0,0 +1,18 @@ +/* + * This settings file was generated by the Gradle 'init' task. + * + * The settings file is used to specify which projects to include in your build. + * In a single project build this file can be empty or even removed. + * + * Detailed information about configuring a multi-project build in Gradle can be found + * in the user guide at https://docs.gradle.org/4.4.1/userguide/multi_project_builds.html + */ + +/* +// To declare projects as part of a multi-project build use the 'include' method +include 'shared' +include 'api' +include 'services:webservice' +*/ + +rootProject.name = 'worker' diff --git a/client/src/main/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerExtensions.java b/azuremanaged/worker/src/main/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerExtensions.java similarity index 100% rename from client/src/main/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerExtensions.java rename to azuremanaged/worker/src/main/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerExtensions.java diff --git a/client/src/main/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerOptions.java b/azuremanaged/worker/src/main/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerOptions.java similarity index 100% rename from client/src/main/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerOptions.java rename to azuremanaged/worker/src/main/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerOptions.java From a7e8d1969b364814ff889c3c6110044fecf6ec88 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 18 Mar 2025 09:04:38 -0700 Subject: [PATCH 18/53] progress --- azuremanaged/build.gradle | 45 ++++++++++++++++++++++ azuremanaged/client/build.gradle | 58 +++++++++++++++++++++++------ azuremanaged/client/settings.gradle | 18 --------- azuremanaged/settings.gradle | 5 +++ azuremanaged/shared/build.gradle | 53 ++++++++++++++++++++++++++ azuremanaged/worker/build.gradle | 58 +++++++++++++++++++++++------ azuremanaged/worker/settings.gradle | 18 --------- 7 files changed, 195 insertions(+), 60 deletions(-) create mode 100644 azuremanaged/build.gradle delete mode 100644 azuremanaged/client/settings.gradle create mode 100644 azuremanaged/settings.gradle create mode 100644 azuremanaged/shared/build.gradle delete mode 100644 azuremanaged/worker/settings.gradle diff --git a/azuremanaged/build.gradle b/azuremanaged/build.gradle new file mode 100644 index 00000000..b94bc800 --- /dev/null +++ b/azuremanaged/build.gradle @@ -0,0 +1,45 @@ +plugins { + id 'java' + id 'idea' +} + +allprojects { + group = 'com.microsoft' + version = '1.5.0' + + repositories { + mavenCentral() + jcenter() + } +} + +subprojects { + apply plugin: 'java' + apply plugin: 'idea' + + compileJava { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + compileTestJava { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + test { + useJUnitPlatform() + } +} + +project(':client') { + dependencies { + implementation project(':shared') + } +} + +project(':worker') { + dependencies { + implementation project(':shared') + } +} \ No newline at end of file diff --git a/azuremanaged/client/build.gradle b/azuremanaged/client/build.gradle index 46867078..cce9926b 100644 --- a/azuremanaged/client/build.gradle +++ b/azuremanaged/client/build.gradle @@ -6,27 +6,61 @@ * user guide available at https://docs.gradle.org/4.4.1/userguide/tutorial_java_projects.html */ -// Apply the java plugin to add support for Java -apply plugin: 'java' +plugins { + id 'java' + id 'application' + id 'com.google.protobuf' version '0.8.16' + id 'idea' +} + +group 'com.microsoft' +version = '1.5.0' +archivesBaseName = 'durabletask-azuremanaged-client' -// Apply the application plugin to add support for building an application -apply plugin: 'application' +def grpcVersion = '1.59.0' +def protocVersion = '3.12.0' +def jacksonVersion = '2.15.3' +def azureCoreVersion = '1.45.0' +def azureIdentityVersion = '1.11.1' -// In this section you declare where to find the dependencies of your project repositories { - // Use jcenter for resolving your dependencies. - // You can declare any Maven/Ivy/file repository here. + mavenCentral() jcenter() } dependencies { - // This dependency is found on compile classpath of this component and consumers. - compile 'com.google.guava:guava:23.0' + implementation "io.grpc:grpc-protobuf:${grpcVersion}" + implementation "io.grpc:grpc-stub:${grpcVersion}" + runtimeOnly "io.grpc:grpc-netty-shaded:${grpcVersion}" + + compileOnly "org.apache.tomcat:annotations-api:6.0.53" + + implementation "com.fasterxml.jackson.core:jackson-core:${jacksonVersion}" + implementation "com.fasterxml.jackson.core:jackson-databind:${jacksonVersion}" + implementation "com.fasterxml.jackson.core:jackson-annotations:${jacksonVersion}" + implementation "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:${jacksonVersion}" + + implementation "com.azure:azure-core:${azureCoreVersion}" + implementation "com.azure:azure-identity:${azureIdentityVersion}" + + testImplementation(platform('org.junit:junit-bom:5.7.2')) + testImplementation('org.junit.jupiter:junit-jupiter') +} + +compileJava { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +compileTestJava { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} - // Use JUnit test framework - testCompile 'junit:junit:4.12' +test { + useJUnitPlatform() } // Define the main class for the application -mainClassName = 'App' +mainClassName = 'com.microsoft.durabletask.azuremanaged.client.AzureManagedClient' diff --git a/azuremanaged/client/settings.gradle b/azuremanaged/client/settings.gradle deleted file mode 100644 index 15547c43..00000000 --- a/azuremanaged/client/settings.gradle +++ /dev/null @@ -1,18 +0,0 @@ -/* - * This settings file was generated by the Gradle 'init' task. - * - * The settings file is used to specify which projects to include in your build. - * In a single project build this file can be empty or even removed. - * - * Detailed information about configuring a multi-project build in Gradle can be found - * in the user guide at https://docs.gradle.org/4.4.1/userguide/multi_project_builds.html - */ - -/* -// To declare projects as part of a multi-project build use the 'include' method -include 'shared' -include 'api' -include 'services:webservice' -*/ - -rootProject.name = 'client' diff --git a/azuremanaged/settings.gradle b/azuremanaged/settings.gradle new file mode 100644 index 00000000..46b71477 --- /dev/null +++ b/azuremanaged/settings.gradle @@ -0,0 +1,5 @@ +rootProject.name = 'durabletask-azuremanaged' + +include 'client' +include 'worker' +include 'shared' \ No newline at end of file diff --git a/azuremanaged/shared/build.gradle b/azuremanaged/shared/build.gradle new file mode 100644 index 00000000..55898aec --- /dev/null +++ b/azuremanaged/shared/build.gradle @@ -0,0 +1,53 @@ +plugins { + id 'java' + id 'com.google.protobuf' version '0.8.16' + id 'idea' +} + +group 'com.microsoft' +version = '1.5.0' +archivesBaseName = 'durabletask-azuremanaged-shared' + +def grpcVersion = '1.59.0' +def protocVersion = '3.12.0' +def jacksonVersion = '2.15.3' +def azureCoreVersion = '1.45.0' +def azureIdentityVersion = '1.11.1' + +repositories { + mavenCentral() + jcenter() +} + +dependencies { + implementation "io.grpc:grpc-protobuf:${grpcVersion}" + implementation "io.grpc:grpc-stub:${grpcVersion}" + runtimeOnly "io.grpc:grpc-netty-shaded:${grpcVersion}" + + compileOnly "org.apache.tomcat:annotations-api:6.0.53" + + implementation "com.fasterxml.jackson.core:jackson-core:${jacksonVersion}" + implementation "com.fasterxml.jackson.core:jackson-databind:${jacksonVersion}" + implementation "com.fasterxml.jackson.core:jackson-annotations:${jacksonVersion}" + implementation "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:${jacksonVersion}" + + implementation "com.azure:azure-core:${azureCoreVersion}" + implementation "com.azure:azure-identity:${azureIdentityVersion}" + + testImplementation(platform('org.junit:junit-bom:5.7.2')) + testImplementation('org.junit.jupiter:junit-jupiter') +} + +compileJava { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +compileTestJava { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/azuremanaged/worker/build.gradle b/azuremanaged/worker/build.gradle index 46867078..5b4031a8 100644 --- a/azuremanaged/worker/build.gradle +++ b/azuremanaged/worker/build.gradle @@ -6,27 +6,61 @@ * user guide available at https://docs.gradle.org/4.4.1/userguide/tutorial_java_projects.html */ -// Apply the java plugin to add support for Java -apply plugin: 'java' +plugins { + id 'java' + id 'application' + id 'com.google.protobuf' version '0.8.16' + id 'idea' +} + +group 'com.microsoft' +version = '1.5.0' +archivesBaseName = 'durabletask-azuremanaged-worker' -// Apply the application plugin to add support for building an application -apply plugin: 'application' +def grpcVersion = '1.59.0' +def protocVersion = '3.12.0' +def jacksonVersion = '2.15.3' +def azureCoreVersion = '1.45.0' +def azureIdentityVersion = '1.11.1' -// In this section you declare where to find the dependencies of your project repositories { - // Use jcenter for resolving your dependencies. - // You can declare any Maven/Ivy/file repository here. + mavenCentral() jcenter() } dependencies { - // This dependency is found on compile classpath of this component and consumers. - compile 'com.google.guava:guava:23.0' + implementation "io.grpc:grpc-protobuf:${grpcVersion}" + implementation "io.grpc:grpc-stub:${grpcVersion}" + runtimeOnly "io.grpc:grpc-netty-shaded:${grpcVersion}" + + compileOnly "org.apache.tomcat:annotations-api:6.0.53" + + implementation "com.fasterxml.jackson.core:jackson-core:${jacksonVersion}" + implementation "com.fasterxml.jackson.core:jackson-databind:${jacksonVersion}" + implementation "com.fasterxml.jackson.core:jackson-annotations:${jacksonVersion}" + implementation "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:${jacksonVersion}" + + implementation "com.azure:azure-core:${azureCoreVersion}" + implementation "com.azure:azure-identity:${azureIdentityVersion}" + + testImplementation(platform('org.junit:junit-bom:5.7.2')) + testImplementation('org.junit.jupiter:junit-jupiter') +} + +compileJava { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +compileTestJava { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} - // Use JUnit test framework - testCompile 'junit:junit:4.12' +test { + useJUnitPlatform() } // Define the main class for the application -mainClassName = 'App' +mainClassName = 'com.microsoft.durabletask.azuremanaged.worker.AzureManagedWorker' diff --git a/azuremanaged/worker/settings.gradle b/azuremanaged/worker/settings.gradle deleted file mode 100644 index dd76947d..00000000 --- a/azuremanaged/worker/settings.gradle +++ /dev/null @@ -1,18 +0,0 @@ -/* - * This settings file was generated by the Gradle 'init' task. - * - * The settings file is used to specify which projects to include in your build. - * In a single project build this file can be empty or even removed. - * - * Detailed information about configuring a multi-project build in Gradle can be found - * in the user guide at https://docs.gradle.org/4.4.1/userguide/multi_project_builds.html - */ - -/* -// To declare projects as part of a multi-project build use the 'include' method -include 'shared' -include 'api' -include 'services:webservice' -*/ - -rootProject.name = 'worker' From ab897852463f822e4d6947c51806aff4edb6ae9f Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 18 Mar 2025 09:05:27 -0700 Subject: [PATCH 19/53] p --- azuremanaged/client/build.gradle | 4 ---- azuremanaged/worker/build.gradle | 4 ---- 2 files changed, 8 deletions(-) diff --git a/azuremanaged/client/build.gradle b/azuremanaged/client/build.gradle index cce9926b..6725584a 100644 --- a/azuremanaged/client/build.gradle +++ b/azuremanaged/client/build.gradle @@ -8,7 +8,6 @@ plugins { id 'java' - id 'application' id 'com.google.protobuf' version '0.8.16' id 'idea' } @@ -61,6 +60,3 @@ test { useJUnitPlatform() } -// Define the main class for the application -mainClassName = 'com.microsoft.durabletask.azuremanaged.client.AzureManagedClient' - diff --git a/azuremanaged/worker/build.gradle b/azuremanaged/worker/build.gradle index 5b4031a8..947d222b 100644 --- a/azuremanaged/worker/build.gradle +++ b/azuremanaged/worker/build.gradle @@ -8,7 +8,6 @@ plugins { id 'java' - id 'application' id 'com.google.protobuf' version '0.8.16' id 'idea' } @@ -61,6 +60,3 @@ test { useJUnitPlatform() } -// Define the main class for the application -mainClassName = 'com.microsoft.durabletask.azuremanaged.worker.AzureManagedWorker' - From 6fc07486ba53043d49c929cbfa60ccd070d2dbfe Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 18 Mar 2025 09:18:22 -0700 Subject: [PATCH 20/53] progress --- azuremanaged/build.gradle | 9 +++++++++ azuremanaged/shared/build.gradle | 7 +++++++ 2 files changed, 16 insertions(+) diff --git a/azuremanaged/build.gradle b/azuremanaged/build.gradle index b94bc800..fdc777e8 100644 --- a/azuremanaged/build.gradle +++ b/azuremanaged/build.gradle @@ -42,4 +42,13 @@ project(':worker') { dependencies { implementation project(':shared') } +} + +project(':shared') { + // Mark shared as internal module + configurations.all { + resolutionStrategy { + disableDependencyVerification() + } + } } \ No newline at end of file diff --git a/azuremanaged/shared/build.gradle b/azuremanaged/shared/build.gradle index 55898aec..a657ddad 100644 --- a/azuremanaged/shared/build.gradle +++ b/azuremanaged/shared/build.gradle @@ -50,4 +50,11 @@ compileTestJava { test { useJUnitPlatform() +} + +// Prevent publishing of shared module +configurations.all { + resolutionStrategy { + disableDependencyVerification() + } } \ No newline at end of file From e3406d7d4a66884db0b44d2288bfee09d954cbe1 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 18 Mar 2025 09:24:50 -0700 Subject: [PATCH 21/53] progress --- azuremanaged/build.gradle | 9 ------ azuremanaged/client/build.gradle | 54 ++++++++++++++++++++++++++++++++ azuremanaged/shared/build.gradle | 7 ----- azuremanaged/worker/build.gradle | 54 ++++++++++++++++++++++++++++++++ 4 files changed, 108 insertions(+), 16 deletions(-) diff --git a/azuremanaged/build.gradle b/azuremanaged/build.gradle index fdc777e8..b94bc800 100644 --- a/azuremanaged/build.gradle +++ b/azuremanaged/build.gradle @@ -42,13 +42,4 @@ project(':worker') { dependencies { implementation project(':shared') } -} - -project(':shared') { - // Mark shared as internal module - configurations.all { - resolutionStrategy { - disableDependencyVerification() - } - } } \ No newline at end of file diff --git a/azuremanaged/client/build.gradle b/azuremanaged/client/build.gradle index 6725584a..cf5e9e68 100644 --- a/azuremanaged/client/build.gradle +++ b/azuremanaged/client/build.gradle @@ -10,6 +10,8 @@ plugins { id 'java' id 'com.google.protobuf' version '0.8.16' id 'idea' + id 'maven-publish' + id 'signing' } group 'com.microsoft' @@ -60,3 +62,55 @@ test { useJUnitPlatform() } +publishing { + repositories { + maven { + url "file://$project.rootDir/repo" + } + } + publications { + mavenJava(MavenPublication) { + from components.java + artifactId = archivesBaseName + pom { + name = 'Durable Task Azure Managed Client SDK for Java' + description = 'This package contains classes and interfaces for building Durable Task orchestrations in Java using Azure Managed mode.' + url = "https://github.com/microsoft/durabletask-java/tree/main/azuremanaged/client" + licenses { + license { + name = "MIT License" + url = "https://opensource.org/licenses/MIT" + distribution = "repo" + } + } + developers { + developer { + id = "Microsoft" + name = "Microsoft Corporation" + } + } + scm { + connection = "scm:git:https://github.com/microsoft/durabletask-java" + developerConnection = "scm:git:git@github.com:microsoft/durabletask-java" + url = "https://github.com/microsoft/durabletask-java/tree/main/azuremanaged/client" + } + withXml { + project.configurations.compileOnly.allDependencies.each { dependency -> + asNode().dependencies[0].appendNode("dependency").with { + it.appendNode("groupId", dependency.group) + it.appendNode("artifactId", dependency.name) + it.appendNode("version", dependency.version) + it.appendNode("scope", "provided") + } + } + } + } + } + } +} + +java { + withSourcesJar() + withJavadocJar() +} + diff --git a/azuremanaged/shared/build.gradle b/azuremanaged/shared/build.gradle index a657ddad..55898aec 100644 --- a/azuremanaged/shared/build.gradle +++ b/azuremanaged/shared/build.gradle @@ -50,11 +50,4 @@ compileTestJava { test { useJUnitPlatform() -} - -// Prevent publishing of shared module -configurations.all { - resolutionStrategy { - disableDependencyVerification() - } } \ No newline at end of file diff --git a/azuremanaged/worker/build.gradle b/azuremanaged/worker/build.gradle index 947d222b..ce69f3b2 100644 --- a/azuremanaged/worker/build.gradle +++ b/azuremanaged/worker/build.gradle @@ -10,6 +10,8 @@ plugins { id 'java' id 'com.google.protobuf' version '0.8.16' id 'idea' + id 'maven-publish' + id 'signing' } group 'com.microsoft' @@ -60,3 +62,55 @@ test { useJUnitPlatform() } +publishing { + repositories { + maven { + url "file://$project.rootDir/repo" + } + } + publications { + mavenJava(MavenPublication) { + from components.java + artifactId = archivesBaseName + pom { + name = 'Durable Task Azure Managed Worker SDK for Java' + description = 'This package contains classes and interfaces for building Durable Task workers in Java using Azure Managed mode.' + url = "https://github.com/microsoft/durabletask-java/tree/main/azuremanaged/worker" + licenses { + license { + name = "MIT License" + url = "https://opensource.org/licenses/MIT" + distribution = "repo" + } + } + developers { + developer { + id = "Microsoft" + name = "Microsoft Corporation" + } + } + scm { + connection = "scm:git:https://github.com/microsoft/durabletask-java" + developerConnection = "scm:git:git@github.com:microsoft/durabletask-java" + url = "https://github.com/microsoft/durabletask-java/tree/main/azuremanaged/worker" + } + withXml { + project.configurations.compileOnly.allDependencies.each { dependency -> + asNode().dependencies[0].appendNode("dependency").with { + it.appendNode("groupId", dependency.group) + it.appendNode("artifactId", dependency.name) + it.appendNode("version", dependency.version) + it.appendNode("scope", "provided") + } + } + } + } + } + } +} + +java { + withSourcesJar() + withJavadocJar() +} + From 110306ea3a99e8c9fde0147eaddfc41812e39419 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 18 Mar 2025 09:38:21 -0700 Subject: [PATCH 22/53] fix --- azuremanaged/build.gradle | 41 -------------------------------- azuremanaged/client/build.gradle | 7 ++++-- azuremanaged/shared/build.gradle | 6 +++-- azuremanaged/worker/build.gradle | 7 ++++-- 4 files changed, 14 insertions(+), 47 deletions(-) diff --git a/azuremanaged/build.gradle b/azuremanaged/build.gradle index b94bc800..c3ecbc84 100644 --- a/azuremanaged/build.gradle +++ b/azuremanaged/build.gradle @@ -1,45 +1,4 @@ -plugins { - id 'java' - id 'idea' -} - allprojects { group = 'com.microsoft' version = '1.5.0' - - repositories { - mavenCentral() - jcenter() - } -} - -subprojects { - apply plugin: 'java' - apply plugin: 'idea' - - compileJava { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - } - - compileTestJava { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - } - - test { - useJUnitPlatform() - } -} - -project(':client') { - dependencies { - implementation project(':shared') - } -} - -project(':worker') { - dependencies { - implementation project(':shared') - } } \ No newline at end of file diff --git a/azuremanaged/client/build.gradle b/azuremanaged/client/build.gradle index cf5e9e68..dda47369 100644 --- a/azuremanaged/client/build.gradle +++ b/azuremanaged/client/build.gradle @@ -30,6 +30,7 @@ repositories { } dependencies { + implementation project(':shared') implementation "io.grpc:grpc-protobuf:${grpcVersion}" implementation "io.grpc:grpc-stub:${grpcVersion}" runtimeOnly "io.grpc:grpc-netty-shaded:${grpcVersion}" @@ -54,8 +55,10 @@ compileJava { } compileTestJava { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + options.fork = true + options.forkOptions.executable = "${PATH_TO_TEST_JAVA_RUNTIME}/bin/javac" } test { diff --git a/azuremanaged/shared/build.gradle b/azuremanaged/shared/build.gradle index 55898aec..ea3d9dba 100644 --- a/azuremanaged/shared/build.gradle +++ b/azuremanaged/shared/build.gradle @@ -44,8 +44,10 @@ compileJava { } compileTestJava { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + options.fork = true + options.forkOptions.executable = "${PATH_TO_TEST_JAVA_RUNTIME}/bin/javac" } test { diff --git a/azuremanaged/worker/build.gradle b/azuremanaged/worker/build.gradle index ce69f3b2..4533c768 100644 --- a/azuremanaged/worker/build.gradle +++ b/azuremanaged/worker/build.gradle @@ -30,6 +30,7 @@ repositories { } dependencies { + implementation project(':shared') implementation "io.grpc:grpc-protobuf:${grpcVersion}" implementation "io.grpc:grpc-stub:${grpcVersion}" runtimeOnly "io.grpc:grpc-netty-shaded:${grpcVersion}" @@ -54,8 +55,10 @@ compileJava { } compileTestJava { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + options.fork = true + options.forkOptions.executable = "${PATH_TO_TEST_JAVA_RUNTIME}/bin/javac" } test { From b7a88af1c2107c471a04fb4058b9b38c933ed776 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 18 Mar 2025 09:57:36 -0700 Subject: [PATCH 23/53] p --- azuremanaged/client/build.gradle | 3 +++ azuremanaged/shared/build.gradle | 3 +++ azuremanaged/worker/build.gradle | 3 +++ 3 files changed, 9 insertions(+) diff --git a/azuremanaged/client/build.gradle b/azuremanaged/client/build.gradle index dda47369..3bacb90d 100644 --- a/azuremanaged/client/build.gradle +++ b/azuremanaged/client/build.gradle @@ -23,6 +23,9 @@ def protocVersion = '3.12.0' def jacksonVersion = '2.15.3' def azureCoreVersion = '1.45.0' def azureIdentityVersion = '1.11.1' +// When build on local, you need to set this value to your local jdk11 directory. +// Java11 is used to compile and run all the tests. +def PATH_TO_TEST_JAVA_RUNTIME = System.env.JDK_11 ?: System.getProperty("java.home") repositories { mavenCentral() diff --git a/azuremanaged/shared/build.gradle b/azuremanaged/shared/build.gradle index ea3d9dba..81bb83b5 100644 --- a/azuremanaged/shared/build.gradle +++ b/azuremanaged/shared/build.gradle @@ -13,6 +13,9 @@ def protocVersion = '3.12.0' def jacksonVersion = '2.15.3' def azureCoreVersion = '1.45.0' def azureIdentityVersion = '1.11.1' +// When build on local, you need to set this value to your local jdk11 directory. +// Java11 is used to compile and run all the tests. +def PATH_TO_TEST_JAVA_RUNTIME = System.env.JDK_11 ?: System.getProperty("java.home") repositories { mavenCentral() diff --git a/azuremanaged/worker/build.gradle b/azuremanaged/worker/build.gradle index 4533c768..bf1f68e9 100644 --- a/azuremanaged/worker/build.gradle +++ b/azuremanaged/worker/build.gradle @@ -23,6 +23,9 @@ def protocVersion = '3.12.0' def jacksonVersion = '2.15.3' def azureCoreVersion = '1.45.0' def azureIdentityVersion = '1.11.1' +// When build on local, you need to set this value to your local jdk11 directory. +// Java11 is used to compile and run all the tests. +def PATH_TO_TEST_JAVA_RUNTIME = System.env.JDK_11 ?: System.getProperty("java.home") repositories { mavenCentral() From 1b7fe1c356a59df5a8181e9e15741ef1859c42a1 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 19 Mar 2025 06:08:32 -0700 Subject: [PATCH 24/53] save --- azuremanaged/client/build.gradle | 2 +- azuremanaged/worker/build.gradle | 2 +- samples/build.gradle | 3 ++- settings.gradle | 5 ++++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/azuremanaged/client/build.gradle b/azuremanaged/client/build.gradle index 3bacb90d..c69806e9 100644 --- a/azuremanaged/client/build.gradle +++ b/azuremanaged/client/build.gradle @@ -33,7 +33,7 @@ repositories { } dependencies { - implementation project(':shared') + implementation project(':azuremanaged:shared') implementation "io.grpc:grpc-protobuf:${grpcVersion}" implementation "io.grpc:grpc-stub:${grpcVersion}" runtimeOnly "io.grpc:grpc-netty-shaded:${grpcVersion}" diff --git a/azuremanaged/worker/build.gradle b/azuremanaged/worker/build.gradle index bf1f68e9..985a4efe 100644 --- a/azuremanaged/worker/build.gradle +++ b/azuremanaged/worker/build.gradle @@ -33,7 +33,7 @@ repositories { } dependencies { - implementation project(':shared') + implementation project(':azuremanaged:shared') implementation "io.grpc:grpc-protobuf:${grpcVersion}" implementation "io.grpc:grpc-stub:${grpcVersion}" runtimeOnly "io.grpc:grpc-netty-shaded:${grpcVersion}" diff --git a/samples/build.gradle b/samples/build.gradle index 2312e7d8..7e41d166 100644 --- a/samples/build.gradle +++ b/samples/build.gradle @@ -14,7 +14,8 @@ application { } dependencies { - implementation project(':client') + implementation project(':azuremanaged:client') + implementation project(':azuremanaged:worker') implementation 'org.springframework.boot:spring-boot-starter-web' implementation platform("org.springframework.boot:spring-boot-dependencies:2.5.2") diff --git a/settings.gradle b/settings.gradle index 5e2f3b65..f5a32602 100644 --- a/settings.gradle +++ b/settings.gradle @@ -2,8 +2,11 @@ rootProject.name = 'durabletask-java' include ":client" include ":azurefunctions" +include ":azuremanaged" +include ":azuremanaged:client" +include ":azuremanaged:worker" +include ":azuremanaged:shared" include ":samples" include ":samples-azure-functions" include ":endtoendtests" -include ":azuremanaged" From b74d6992db5d9b49d4543e1ac584c87a0cc51c16 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 19 Mar 2025 06:33:17 -0700 Subject: [PATCH 25/53] save save --- .../DurableTaskSchedulerClientOptions.java | 152 ++++++++++++----- .../DurableTaskSchedulerConnectionString.java | 154 +++++++++--------- .../DurableTaskSchedulerWorkerExtensions.java | 110 +++++++++++++ 3 files changed, 296 insertions(+), 120 deletions(-) create mode 100644 azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/worker/DurableTaskSchedulerWorkerExtensions.java diff --git a/azuremanaged/client/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientOptions.java b/azuremanaged/client/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientOptions.java index ff0a99ca..7258cb6c 100644 --- a/azuremanaged/client/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientOptions.java +++ b/azuremanaged/client/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientOptions.java @@ -5,19 +5,28 @@ import com.azure.core.credential.TokenCredential; import com.microsoft.durabletask.shared.azuremanaged.DurableTaskSchedulerConnectionString; +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import io.grpc.Metadata; +import io.grpc.stub.MetadataUtils; +import jakarta.validation.constraints.NotBlank; import java.time.Duration; import java.util.Objects; /** - * Configuration options for connecting to Azure-managed Durable Task Scheduler as a client. + * Options for configuring the Durable Task Scheduler. */ public class DurableTaskSchedulerClientOptions { - private String endpoint; - private String taskHubName; + @NotBlank(message = "Endpoint address is required") + private String endpointAddress = ""; + + @NotBlank(message = "Task hub name is required") + private String taskHubName = ""; + + private TokenCredential credential; private String resourceId = "https://durabletask.io"; - private boolean allowInsecure = false; - private TokenCredential tokenCredential; + private boolean allowInsecureCredentials = false; private Duration tokenRefreshMargin = Duration.ofMinutes(5); /** @@ -34,33 +43,42 @@ public DurableTaskSchedulerClientOptions() { */ public static DurableTaskSchedulerClientOptions fromConnectionString(String connectionString) { DurableTaskSchedulerConnectionString parsedConnectionString = DurableTaskSchedulerConnectionString.parse(connectionString); - + return fromConnectionString(parsedConnectionString); + } + + /** + * Creates a new instance of DurableTaskSchedulerClientOptions from a parsed connection string. + * + * @param connectionString The parsed connection string. + * @return A new DurableTaskSchedulerClientOptions object. + */ + static DurableTaskSchedulerClientOptions fromConnectionString(DurableTaskSchedulerConnectionString connectionString) { + TokenCredential credential = getCredentialFromConnectionString(connectionString); DurableTaskSchedulerClientOptions options = new DurableTaskSchedulerClientOptions(); - options.setEndpoint(parsedConnectionString.getEndpoint()); - options.setTaskHubName(parsedConnectionString.getTaskHubName()); - options.setResourceId(parsedConnectionString.getResourceId()); - options.setAllowInsecure(parsedConnectionString.isAllowInsecure()); - + options.setEndpointAddress(connectionString.getEndpoint()); + options.setTaskHubName(connectionString.getTaskHubName()); + options.setCredential(credential); + options.setAllowInsecureCredentials(credential == null); return options; } /** - * Gets the endpoint URL. + * Gets the endpoint address. * - * @return The endpoint URL. + * @return The endpoint address. */ - public String getEndpoint() { - return endpoint; + public String getEndpointAddress() { + return endpointAddress; } /** - * Sets the endpoint URL. + * Sets the endpoint address. * - * @param endpoint The endpoint URL. + * @param endpointAddress The endpoint address. * @return This options object. */ - public DurableTaskSchedulerClientOptions setEndpoint(String endpoint) { - this.endpoint = endpoint; + public DurableTaskSchedulerClientOptions setEndpointAddress(String endpointAddress) { + this.endpointAddress = endpointAddress; return this; } @@ -85,62 +103,62 @@ public DurableTaskSchedulerClientOptions setTaskHubName(String taskHubName) { } /** - * Gets the resource ID for authentication. + * Gets the credential used for authentication. * - * @return The resource ID. + * @return The credential. */ - public String getResourceId() { - return resourceId; + public TokenCredential getCredential() { + return credential; } /** - * Sets the resource ID for authentication. + * Sets the credential used for authentication. * - * @param resourceId The resource ID. + * @param credential The credential. * @return This options object. */ - public DurableTaskSchedulerClientOptions setResourceId(String resourceId) { - this.resourceId = resourceId; + public DurableTaskSchedulerClientOptions setCredential(TokenCredential credential) { + this.credential = credential; return this; } /** - * Gets whether insecure connections are allowed. + * Gets the resource ID. * - * @return True if insecure connections are allowed, false otherwise. + * @return The resource ID. */ - public boolean isAllowInsecure() { - return allowInsecure; + public String getResourceId() { + return resourceId; } /** - * Sets whether insecure connections are allowed. + * Sets the resource ID. * - * @param allowInsecure True to allow insecure connections, false otherwise. + * @param resourceId The resource ID. * @return This options object. */ - public DurableTaskSchedulerClientOptions setAllowInsecure(boolean allowInsecure) { - this.allowInsecure = allowInsecure; + public DurableTaskSchedulerClientOptions setResourceId(String resourceId) { + this.resourceId = resourceId; return this; } /** - * Gets the token credential for authentication. + * Gets whether insecure credentials are allowed. * - * @return The token credential. + * @return True if insecure credentials are allowed. */ - public TokenCredential getTokenCredential() { - return tokenCredential; + public boolean isAllowInsecureCredentials() { + return allowInsecureCredentials; } /** - * Sets the token credential for authentication. + * Sets whether insecure credentials are allowed. * - * @param tokenCredential The token credential. + * @param allowInsecureCredentials True to allow insecure credentials. * @return This options object. */ - public DurableTaskSchedulerClientOptions setTokenCredential(TokenCredential tokenCredential) { - this.tokenCredential = tokenCredential; + public DurableTaskSchedulerClientOptions setAllowInsecureCredentials(boolean allowInsecureCredentials) { + this.allowInsecureCredentials = allowInsecureCredentials; return this; } @@ -164,13 +182,57 @@ public DurableTaskSchedulerClientOptions setTokenRefreshMargin(Duration tokenRef return this; } + /** + * Creates a gRPC channel for communicating with the Durable Task Scheduler service. + * + * @return A configured ManagedChannel instance. + */ + public ManagedChannel createChannel() { + Objects.requireNonNull(endpointAddress, "endpointAddress must not be null"); + Objects.requireNonNull(taskHubName, "taskHubName must not be null"); + + String endpoint = !endpointAddress.contains("://") ? "https://" + endpointAddress : endpointAddress; + + Metadata metadata = new Metadata(); + metadata.put(Metadata.Key.of("taskhub", Metadata.ASCII_STRING_MARSHALLER), taskHubName); + + ManagedChannelBuilder channelBuilder = ManagedChannelBuilder.forTarget(endpoint); + + if (endpoint.startsWith("https://")) { + channelBuilder.useTransportSecurity(); + } else { + channelBuilder.usePlaintext(); + } + + if (credential != null) { + // TODO: Implement token credential handling for gRPC + // This would require implementing a custom CallCredentials class + } + + return channelBuilder.build(); + } + + /** + * Gets the credential from a connection string. + * + * @param connectionString The connection string. + * @return The credential. + */ + private static TokenCredential getCredentialFromConnectionString(DurableTaskSchedulerConnectionString connectionString) { + String authType = connectionString.getAuthentication().toLowerCase(); + + // TODO: Implement credential creation based on auth type + // This would require implementing the various credential types + return null; + } + /** * Validates that the options are properly configured. * * @throws IllegalArgumentException If the options are not properly configured. */ public void validate() { - Objects.requireNonNull(endpoint, "endpoint must not be null"); + Objects.requireNonNull(endpointAddress, "endpointAddress must not be null"); Objects.requireNonNull(taskHubName, "taskHubName must not be null"); } -} \ No newline at end of file +} \ No newline at end of file diff --git a/azuremanaged/shared/DurableTaskSchedulerConnectionString.java b/azuremanaged/shared/DurableTaskSchedulerConnectionString.java index 4236468f..dd993590 100644 --- a/azuremanaged/shared/DurableTaskSchedulerConnectionString.java +++ b/azuremanaged/shared/DurableTaskSchedulerConnectionString.java @@ -3,120 +3,124 @@ package com.microsoft.durabletask.shared.azuremanaged; +import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Objects; /** - * Parses and manages connection strings for Azure-managed Durable Task Scheduler. - * This class is used by both client and worker components. + * Represents the constituent parts of a connection string for a Durable Task Scheduler service. */ public class DurableTaskSchedulerConnectionString { - private final String endpoint; - private final String taskHubName; - private final String resourceId; - private final boolean allowInsecure; + private final Map properties; /** - * Creates a new instance of DurableTaskSchedulerConnectionString. + * Initializes a new instance of the DurableTaskSchedulerConnectionString class. * - * @param endpoint The endpoint URL of the Durable Task Scheduler service. - * @param taskHubName The name of the task hub. - * @param resourceId The Azure resource ID for authentication. - * @param allowInsecure Whether to allow insecure connections. + * @param connectionString A connection string for a Durable Task Scheduler service. + * @throws IllegalArgumentException If the connection string is invalid or missing required properties. */ - public DurableTaskSchedulerConnectionString( - String endpoint, - String taskHubName, - String resourceId, - boolean allowInsecure) { - this.endpoint = Objects.requireNonNull(endpoint, "endpoint must not be null"); - this.taskHubName = Objects.requireNonNull(taskHubName, "taskHubName must not be null"); - this.resourceId = resourceId != null ? resourceId : "https://durabletask.io"; - this.allowInsecure = allowInsecure; + public DurableTaskSchedulerConnectionString(String connectionString) { + if (connectionString == null || connectionString.isEmpty()) { + throw new IllegalArgumentException("connectionString must not be null or empty"); + } + this.properties = parseConnectionString(connectionString); } /** - * Parses a connection string into a DurableTaskSchedulerConnectionString object. + * Gets the authentication method specified in the connection string. * - * @param connectionString The connection string to parse. - * @return A new DurableTaskSchedulerConnectionString object. - * @throws IllegalArgumentException If the connection string is invalid. + * @return The authentication method. */ - public static DurableTaskSchedulerConnectionString parse(String connectionString) { - if (connectionString == null || connectionString.isEmpty()) { - throw new IllegalArgumentException("connectionString must not be null or empty"); - } - - Map properties = parseConnectionString(connectionString); - - String endpoint = properties.get("Endpoint"); - if (endpoint == null || endpoint.isEmpty()) { - throw new IllegalArgumentException("The connection string must contain an Endpoint property"); - } - - String taskHubName = properties.get("TaskHubName"); - if (taskHubName == null || taskHubName.isEmpty()) { - throw new IllegalArgumentException("The connection string must contain a TaskHubName property"); - } - - String resourceId = properties.get("ResourceId"); - - String allowInsecureStr = properties.get("AllowInsecure"); - boolean allowInsecure = "true".equalsIgnoreCase(allowInsecureStr); + public String getAuthentication() { + return getRequiredValue("Authentication"); + } - return new DurableTaskSchedulerConnectionString(endpoint, taskHubName, resourceId, allowInsecure); + /** + * Gets the managed identity or workload identity client ID specified in the connection string. + * + * @return The client ID, or null if not specified. + */ + public String getClientId() { + return getValue("ClientID"); } - private static Map parseConnectionString(String connectionString) { - Map properties = new HashMap<>(); - - String[] pairs = connectionString.split(";"); - for (String pair : pairs) { - int equalsIndex = pair.indexOf('='); - if (equalsIndex > 0) { - String key = pair.substring(0, equalsIndex).trim(); - String value = pair.substring(equalsIndex + 1).trim(); - properties.put(key, value); - } + /** + * Gets the "AdditionallyAllowedTenants" property, optionally used by Workload Identity. + * Multiple values can be separated by a comma. + * + * @return List of allowed tenants, or null if not specified. + */ + public List getAdditionallyAllowedTenants() { + String value = getValue("AdditionallyAllowedTenants"); + if (value == null || value.isEmpty()) { + return null; } - - return properties; + return Arrays.asList(value.split(",")); } /** - * Gets the endpoint URL. + * Gets the "TenantId" property, optionally used by Workload Identity. * - * @return The endpoint URL. + * @return The tenant ID, or null if not specified. */ - public String getEndpoint() { - return endpoint; + public String getTenantId() { + return getValue("TenantId"); } /** - * Gets the task hub name. + * Gets the "TokenFilePath" property, optionally used by Workload Identity. * - * @return The task hub name. + * @return The token file path, or null if not specified. */ - public String getTaskHubName() { - return taskHubName; + public String getTokenFilePath() { + return getValue("TokenFilePath"); } /** - * Gets the resource ID for authentication. + * Gets the endpoint specified in the connection string. * - * @return The resource ID. + * @return The endpoint URL. */ - public String getResourceId() { - return resourceId; + public String getEndpoint() { + return getRequiredValue("Endpoint"); } /** - * Gets whether insecure connections are allowed. + * Gets the task hub name specified in the connection string. * - * @return True if insecure connections are allowed, false otherwise. + * @return The task hub name. */ - public boolean isAllowInsecure() { - return allowInsecure; + public String getTaskHubName() { + return getRequiredValue("TaskHub"); + } + + private String getValue(String name) { + return properties.get(name); + } + + private String getRequiredValue(String name) { + String value = getValue(name); + if (value == null || value.isEmpty()) { + throw new IllegalArgumentException("The connection string must contain a " + name + " property"); + } + return value; + } + + private static Map parseConnectionString(String connectionString) { + Map properties = new HashMap<>(); + + String[] pairs = connectionString.split(";"); + for (String pair : pairs) { + int equalsIndex = pair.indexOf('='); + if (equalsIndex > 0) { + String key = pair.substring(0, equalsIndex).trim(); + String value = pair.substring(equalsIndex + 1).trim(); + properties.put(key, value); + } + } + + return properties; } } \ No newline at end of file diff --git a/azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/worker/DurableTaskSchedulerWorkerExtensions.java b/azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/worker/DurableTaskSchedulerWorkerExtensions.java new file mode 100644 index 00000000..dc377144 --- /dev/null +++ b/azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/worker/DurableTaskSchedulerWorkerExtensions.java @@ -0,0 +1,110 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.durabletask.azuremanaged.worker; + +import com.azure.core.credential.TokenCredential; +import com.azure.core.credential.TokenRequestContext; +import com.microsoft.durabletask.DurableTaskGrpcWorker; +import com.microsoft.durabletask.DurableTaskGrpcWorkerBuilder; +import com.microsoft.durabletask.azuremanaged.shared.AccessTokenCache; +import com.microsoft.durabletask.azuremanaged.shared.DurableTaskSchedulerConnectionString; +import io.grpc.Channel; +import io.grpc.ClientCall; +import io.grpc.ClientInterceptor; +import io.grpc.ForwardingClientCall; +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import io.grpc.Metadata; +import io.grpc.MethodDescriptor; +import io.grpc.CallOptions; +import io.grpc.netty.GrpcSslContexts; +import io.grpc.netty.NettyChannelBuilder; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; + +import javax.net.ssl.SSLException; +import java.time.Duration; +import java.util.Objects; + +/** + * Extension methods for creating DurableTaskGrpcWorker instances that connect to Azure-managed Durable Task Scheduler. + */ +public class DurableTaskSchedulerWorkerExtensions { + + /** + * Creates a DurableTaskGrpcWorkerBuilder that connects to Azure-managed Durable Task Scheduler. + * + * @param options The options for connecting to Azure-managed Durable Task Scheduler. + * @return A new DurableTaskGrpcWorkerBuilder instance. + */ + public static DurableTaskGrpcWorkerBuilder createWorkerBuilder(DurableTaskSchedulerWorkerOptions options) { + Objects.requireNonNull(options, "options must not be null"); + options.validate(); + + // Create the access token cache if credentials are provided + AccessTokenCache tokenCache = null; + TokenCredential credential = options.getTokenCredential(); + if (credential != null) { + TokenRequestContext context = new TokenRequestContext(); + context.addScopes(new String[] { options.getResourceId() + "/.default" }); + tokenCache = new AccessTokenCache(credential, context, options.getTokenRefreshMargin()); + } + + // Create the gRPC channel + Channel grpcChannel = createGrpcChannel(options, tokenCache); + + // Create and return the worker builder + return new DurableTaskGrpcWorkerBuilder() + .grpcChannel(grpcChannel); + } + + static Channel createGrpcChannel(DurableTaskSchedulerWorkerOptions options, AccessTokenCache tokenCache) { + // Normalize the endpoint URL and add DNS scheme for gRPC name resolution + String endpoint = "dns:///" + options.getEndpoint(); + + // Create metadata interceptor to add task hub name and auth token + ClientInterceptor metadataInterceptor = new ClientInterceptor() { + @Override + public ClientCall interceptCall( + MethodDescriptor method, + CallOptions callOptions, + Channel next) { + return new ForwardingClientCall.SimpleForwardingClientCall( + next.newCall(method, callOptions)) { + @Override + public void start(ClientCall.Listener responseListener, Metadata headers) { + headers.put( + Metadata.Key.of("taskhub", Metadata.ASCII_STRING_MARSHALLER), + options.getTaskHubName() + ); + + // Add authorization token if credentials are configured + if (tokenCache != null) { + String token = tokenCache.getToken().getToken(); + headers.put( + Metadata.Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER), + "Bearer " + token + ); + } + + super.start(responseListener, headers); + } + }; + } + }; + + // Build the channel with appropriate security settings + ManagedChannelBuilder builder = ManagedChannelBuilder.forTarget(endpoint) + .intercept(metadataInterceptor); + + if (!options.isAllowInsecure()) { + builder.useTransportSecurity(); + } else { + builder.usePlaintext(); + } + + return builder.build(); + } +} \ No newline at end of file From f6611af171a0271fa4d6c613f27eba8c1bb2a892 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 19 Mar 2025 07:12:08 -0700 Subject: [PATCH 26/53] save --- .../DurableTaskSchedulerClientExtensions.java | 48 -------- .../DurableTaskSchedulerClientOptions.java | 108 ++++++++++-------- .../WebAppToDurableTaskSchedulerSample.java | 2 +- 3 files changed, 64 insertions(+), 94 deletions(-) diff --git a/azuremanaged/client/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientExtensions.java b/azuremanaged/client/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientExtensions.java index b3c62571..4306cdad 100644 --- a/azuremanaged/client/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientExtensions.java +++ b/azuremanaged/client/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientExtensions.java @@ -52,52 +52,4 @@ public static DurableTaskClient createClient(DurableTaskSchedulerClientOptions o .grpcChannel(grpcChannel) .build(); } - - private static Channel createGrpcChannel(DurableTaskSchedulerClientOptions options, AccessTokenCache tokenCache) { - // Normalize the endpoint URL and add DNS scheme for gRPC name resolution - String endpoint = "dns:///" + options.getEndpoint(); - - // Create metadata interceptor to add task hub name and auth token - ClientInterceptor metadataInterceptor = new ClientInterceptor() { - @Override - public ClientCall interceptCall( - MethodDescriptor method, - CallOptions callOptions, - Channel next) { - return new ForwardingClientCall.SimpleForwardingClientCall( - next.newCall(method, callOptions)) { - @Override - public void start(ClientCall.Listener responseListener, Metadata headers) { - headers.put( - Metadata.Key.of("taskhub", Metadata.ASCII_STRING_MARSHALLER), - options.getTaskHubName() - ); - - // Add authorization token if credentials are configured - if (tokenCache != null) { - String token = tokenCache.getToken().getToken(); - headers.put( - Metadata.Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER), - "Bearer " + token - ); - } - - super.start(responseListener, headers); - } - }; - } - }; - - // Build the channel with appropriate security settings - ManagedChannelBuilder builder = ManagedChannelBuilder.forTarget(endpoint) - .intercept(metadataInterceptor); - - if (!options.isAllowInsecure()) { - builder.useTransportSecurity(); - } else { - builder.usePlaintext(); - } - - return builder.build(); - } } \ No newline at end of file diff --git a/azuremanaged/client/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientOptions.java b/azuremanaged/client/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientOptions.java index 7258cb6c..7cfbf477 100644 --- a/azuremanaged/client/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientOptions.java +++ b/azuremanaged/client/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientOptions.java @@ -4,6 +4,13 @@ package com.microsoft.durabletask.client.azuremanaged; import com.azure.core.credential.TokenCredential; +import com.azure.identity.DefaultAzureCredential; +import com.azure.identity.ManagedIdentityCredential; +import com.azure.identity.WorkloadIdentityCredential; +import com.azure.identity.WorkloadIdentityCredentialOptions; +import com.azure.identity.EnvironmentCredential; +import com.azure.identity.AzureCliCredential; +import com.azure.identity.AzurePowerShellCredential; import com.microsoft.durabletask.shared.azuremanaged.DurableTaskSchedulerConnectionString; import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; @@ -41,9 +48,9 @@ public DurableTaskSchedulerClientOptions() { * @param connectionString The connection string to parse. * @return A new DurableTaskSchedulerClientOptions object. */ - public static DurableTaskSchedulerClientOptions fromConnectionString(String connectionString) { + public static DurableTaskSchedulerClientOptions fromConnectionString(String connectionString, @Nullable TokenCredential credential) { DurableTaskSchedulerConnectionString parsedConnectionString = DurableTaskSchedulerConnectionString.parse(connectionString); - return fromConnectionString(parsedConnectionString); + return fromConnectionString(parsedConnectionString, credential); } /** @@ -52,8 +59,11 @@ public static DurableTaskSchedulerClientOptions fromConnectionString(String conn * @param connectionString The parsed connection string. * @return A new DurableTaskSchedulerClientOptions object. */ - static DurableTaskSchedulerClientOptions fromConnectionString(DurableTaskSchedulerConnectionString connectionString) { - TokenCredential credential = getCredentialFromConnectionString(connectionString); + static DurableTaskSchedulerClientOptions fromConnectionString(DurableTaskSchedulerConnectionString connectionString, @Nullable TokenCredential credential) { + // TODO: Parse different credential types from connection string + if (credential == null) { + credential = new DefaultAzureCredentialBuilder().build(); + } DurableTaskSchedulerClientOptions options = new DurableTaskSchedulerClientOptions(); options.setEndpointAddress(connectionString.getEndpoint()); options.setTaskHubName(connectionString.getTaskHubName()); @@ -183,56 +193,64 @@ public DurableTaskSchedulerClientOptions setTokenRefreshMargin(Duration tokenRef } /** - * Creates a gRPC channel for communicating with the Durable Task Scheduler service. + * Validates that the options are properly configured. * - * @return A configured ManagedChannel instance. + * @throws IllegalArgumentException If the options are not properly configured. */ - public ManagedChannel createChannel() { + public void validate() { Objects.requireNonNull(endpointAddress, "endpointAddress must not be null"); Objects.requireNonNull(taskHubName, "taskHubName must not be null"); + } - String endpoint = !endpointAddress.contains("://") ? "https://" + endpointAddress : endpointAddress; - - Metadata metadata = new Metadata(); - metadata.put(Metadata.Key.of("taskhub", Metadata.ASCII_STRING_MARSHALLER), taskHubName); + private static Channel createGrpcChannel() { + TokenRequestContext context = new TokenRequestContext(); + context.addScopes(new String[] { this.resourceId + "/.default" }); + AccessTokenCache tokenCache = new AccessTokenCache(this.credential, context, this.tokenRefreshMargin); + + // Normalize the endpoint URL and add DNS scheme for gRPC name resolution + String endpoint = "dns:///" + this.endpointAddress; - ManagedChannelBuilder channelBuilder = ManagedChannelBuilder.forTarget(endpoint); + // Create metadata interceptor to add task hub name and auth token + ClientInterceptor metadataInterceptor = new ClientInterceptor() { + @Override + public ClientCall interceptCall( + MethodDescriptor method, + CallOptions callOptions, + Channel next) { + return new ForwardingClientCall.SimpleForwardingClientCall( + next.newCall(method, callOptions)) { + @Override + public void start(ClientCall.Listener responseListener, Metadata headers) { + headers.put( + Metadata.Key.of("taskhub", Metadata.ASCII_STRING_MARSHALLER), + this.taskHubName + ); + + // Add authorization token if credentials are configured + if (tokenCache != null) { + String token = tokenCache.getToken().getToken(); + headers.put( + Metadata.Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER), + "Bearer " + token + ); + } + + super.start(responseListener, headers); + } + }; + } + }; - if (endpoint.startsWith("https://")) { - channelBuilder.useTransportSecurity(); + // Build the channel with appropriate security settings + ManagedChannelBuilder builder = ManagedChannelBuilder.forTarget(endpoint) + .intercept(metadataInterceptor); + + if (!options.isAllowInsecure()) { + builder.useTransportSecurity(); } else { - channelBuilder.usePlaintext(); + builder.usePlaintext(); } - if (credential != null) { - // TODO: Implement token credential handling for gRPC - // This would require implementing a custom CallCredentials class - } - - return channelBuilder.build(); - } - - /** - * Gets the credential from a connection string. - * - * @param connectionString The connection string. - * @return The credential. - */ - private static TokenCredential getCredentialFromConnectionString(DurableTaskSchedulerConnectionString connectionString) { - String authType = connectionString.getAuthentication().toLowerCase(); - - // TODO: Implement credential creation based on auth type - // This would require implementing the various credential types - return null; - } - - /** - * Validates that the options are properly configured. - * - * @throws IllegalArgumentException If the options are not properly configured. - */ - public void validate() { - Objects.requireNonNull(endpointAddress, "endpointAddress must not be null"); - Objects.requireNonNull(taskHubName, "taskHubName must not be null"); + return builder.build(); } } \ No newline at end of file diff --git a/samples/src/main/java/io/durabletask/samples/WebAppToDurableTaskSchedulerSample.java b/samples/src/main/java/io/durabletask/samples/WebAppToDurableTaskSchedulerSample.java index d2c73978..4f2c3ee2 100644 --- a/samples/src/main/java/io/durabletask/samples/WebAppToDurableTaskSchedulerSample.java +++ b/samples/src/main/java/io/durabletask/samples/WebAppToDurableTaskSchedulerSample.java @@ -79,7 +79,7 @@ public AccessTokenCache accessTokenCache( ); } - @Bean(destroyMethod = "stop") + @Bean public DurableTaskGrpcWorker durableTaskWorker( DurableTaskProperties properties, TokenCredential tokenCredential) { From 03df0943ffb13943d56d27d19a2177239dfda67c Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 19 Mar 2025 07:41:02 -0700 Subject: [PATCH 27/53] save --- .../DurableTaskSchedulerClientOptions.java | 37 +++++++++++++------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/azuremanaged/client/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientOptions.java b/azuremanaged/client/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientOptions.java index 7cfbf477..be3e51c0 100644 --- a/azuremanaged/client/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientOptions.java +++ b/azuremanaged/client/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientOptions.java @@ -20,6 +20,8 @@ import jakarta.validation.constraints.NotBlank; import java.time.Duration; import java.util.Objects; +import java.net.URL; +import java.net.MalformedURLException; /** * Options for configuring the Durable Task Scheduler. @@ -61,9 +63,6 @@ public static DurableTaskSchedulerClientOptions fromConnectionString(String conn */ static DurableTaskSchedulerClientOptions fromConnectionString(DurableTaskSchedulerConnectionString connectionString, @Nullable TokenCredential credential) { // TODO: Parse different credential types from connection string - if (credential == null) { - credential = new DefaultAzureCredentialBuilder().build(); - } DurableTaskSchedulerClientOptions options = new DurableTaskSchedulerClientOptions(); options.setEndpointAddress(connectionString.getEndpoint()); options.setTaskHubName(connectionString.getTaskHubName()); @@ -203,13 +202,27 @@ public void validate() { } private static Channel createGrpcChannel() { - TokenRequestContext context = new TokenRequestContext(); - context.addScopes(new String[] { this.resourceId + "/.default" }); - AccessTokenCache tokenCache = new AccessTokenCache(this.credential, context, this.tokenRefreshMargin); - - // Normalize the endpoint URL and add DNS scheme for gRPC name resolution - String endpoint = "dns:///" + this.endpointAddress; + // Create token cache only if credential is not null + AccessTokenCache tokenCache = null; + if (credential != null) { + TokenRequestContext context = new TokenRequestContext(); + context.addScopes(new String[] { this.resourceId + "/.default" }); + tokenCache = new AccessTokenCache(this.credential, context, this.tokenRefreshMargin); + } + // Parse and normalize the endpoint URL + String endpoint = endpointAddress; + // Add https:// prefix if no protocol is specified + if (!endpoint.startsWith("http://") && !endpoint.startsWith("https://")) { + endpoint = "https://" + endpoint; + } + + URL url = new URL(endpoint); + String authority = url.getHost(); + if (url.getPort() != -1) { + authority += ":" + url.getPort(); + } + // Create metadata interceptor to add task hub name and auth token ClientInterceptor metadataInterceptor = new ClientInterceptor() { @Override @@ -223,7 +236,7 @@ public ClientCall interceptCall( public void start(ClientCall.Listener responseListener, Metadata headers) { headers.put( Metadata.Key.of("taskhub", Metadata.ASCII_STRING_MARSHALLER), - this.taskHubName + taskHubName ); // Add authorization token if credentials are configured @@ -242,10 +255,10 @@ public void start(ClientCall.Listener responseListener, Metadata headers) }; // Build the channel with appropriate security settings - ManagedChannelBuilder builder = ManagedChannelBuilder.forTarget(endpoint) + ManagedChannelBuilder builder = ManagedChannelBuilder.forTarget(authority) .intercept(metadataInterceptor); - if (!options.isAllowInsecure()) { + if (!endpoint.startsWith("https://")) { builder.useTransportSecurity(); } else { builder.usePlaintext(); From 3cd359867cbf25517e7ae7d707f40ddf8c7a35c4 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 19 Mar 2025 08:05:30 -0700 Subject: [PATCH 28/53] clientoptions done --- .../DurableTaskSchedulerClientOptions.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/azuremanaged/client/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientOptions.java b/azuremanaged/client/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientOptions.java index be3e51c0..629f6995 100644 --- a/azuremanaged/client/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientOptions.java +++ b/azuremanaged/client/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientOptions.java @@ -254,16 +254,16 @@ public void start(ClientCall.Listener responseListener, Metadata headers) } }; - // Build the channel with appropriate security settings - ManagedChannelBuilder builder = ManagedChannelBuilder.forTarget(authority) - .intercept(metadataInterceptor); - - if (!endpoint.startsWith("https://")) { - builder.useTransportSecurity(); + ChannelCredentials credentials; + if (!this.allowInsecure) { + credentials = io.grpc.TlsChannelCredentials.create(); } else { - builder.usePlaintext(); + credentials = InsecureChannelCredentials.create(); } - return builder.build(); + // Create channel with credentials + return Grpc.newChannelBuilder(authority, credentials) + .intercept(metadataInterceptor) + .build(); } } \ No newline at end of file From 357ecdd160cddfff40b19a87b2544f32b566409a Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 19 Mar 2025 08:49:56 -0700 Subject: [PATCH 29/53] client ext --- .../DurableTaskSchedulerClientExtensions.java | 92 +++++++++++++++---- .../DurableTaskSchedulerClientOptions.java | 2 +- 2 files changed, 73 insertions(+), 21 deletions(-) diff --git a/azuremanaged/client/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientExtensions.java b/azuremanaged/client/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientExtensions.java index 4306cdad..436067a5 100644 --- a/azuremanaged/client/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientExtensions.java +++ b/azuremanaged/client/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientExtensions.java @@ -24,32 +24,84 @@ * Extension methods for creating DurableTaskClient instances that connect to Azure-managed Durable Task Scheduler. */ public class DurableTaskSchedulerClientExtensions { - /** - * Creates a DurableTaskClient that connects to Azure-managed Durable Task Scheduler. + * Creates a DurableTaskClient that connects to Azure-managed Durable Task Scheduler using a connection string. * - * @param options The options for connecting to Azure-managed Durable Task Scheduler. + * @param connectionString The connection string for connecting to Azure-managed Durable Task Scheduler. + * @param tokenCredential The token credential for connecting to Azure-managed Durable Task Scheduler. * @return A new DurableTaskClient instance. */ - public static DurableTaskClient createClient(DurableTaskSchedulerClientOptions options) { - Objects.requireNonNull(options, "options must not be null"); - options.validate(); - - // Create the access token cache if credentials are provided - AccessTokenCache tokenCache = null; - TokenCredential credential = options.getTokenCredential(); - if (credential != null) { - TokenRequestContext context = new TokenRequestContext(); - context.addScopes(new String[] { options.getResourceId() + "/.default" }); - tokenCache = new AccessTokenCache(credential, context, options.getTokenRefreshMargin()); - } - - // Create the gRPC channel - Channel grpcChannel = createGrpcChannel(options, tokenCache); - - // Create and return the client + public static DurableTaskClient createClient(String connectionString, @Nullable TokenCredential tokenCredential) { + Objects.requireNonNull(connectionString, "connectionString must not be null"); + DurableTaskSchedulerClientOptions options = DurableTaskSchedulerClientOptions.fromConnectionString(connectionString, tokenCredential); + Channel grpcChannel = options.createGrpcChannel(); + + return new DurableTaskGrpcClientBuilder() + .grpcChannel(grpcChannel) + .build(); + } + + public static DurableTaskClient createClient(String endpoint, String taskHubName, @Nullable TokenCredential tokenCredential) { + DurableTaskSchedulerClientOptions options = new DurableTaskSchedulerClientOptions() + .setEndpoint(endpoint) + .setTaskHubName(taskHubName) + .setTokenCredential(tokenCredential); + + Channel grpcChannel = options.createGrpcChannel(); + return new DurableTaskGrpcClientBuilder() .grpcChannel(grpcChannel) .build(); } + + public static void UseDurableTaskScheduler(DurableTaskGrpcClientBuilder builder, String endpoint, String taskHubName, @Nullable TokenCredential tokenCredential) { + DurableTaskSchedulerClientOptions options = new DurableTaskSchedulerClientOptions() + .setEndpoint(endpoint) + .setTaskHubName(taskHubName) + .setTokenCredential(tokenCredential); + + Channel grpcChannel = options.createGrpcChannel(); + builder.grpcChannel(grpcChannel); + } + public static void UseDurableTaskScheduler(DurableTaskGrpcClientBuilder builder, String connectionString, @Nullable TokenCredential tokenCredential) { + DurableTaskSchedulerClientOptions options = DurableTaskSchedulerClientOptions.fromConnectionString(connectionString, tokenCredential); + Channel grpcChannel = options.createGrpcChannel(); + builder.grpcChannel(grpcChannel); + } + + /** + * Creates a DurableTaskGrpcClientBuilder that connects to Azure-managed Durable Task Scheduler using a connection string. + * + * @param connectionString The connection string for connecting to Azure-managed Durable Task Scheduler. + * @param tokenCredential The token credential for connecting to Azure-managed Durable Task Scheduler. + * @return A new DurableTaskGrpcClientBuilder instance. + */ + public static DurableTaskGrpcClientBuilder createClientBuilder(String connectionString, @Nullable TokenCredential tokenCredential) { + Objects.requireNonNull(connectionString, "connectionString must not be null"); + DurableTaskSchedulerClientOptions options = DurableTaskSchedulerClientOptions.fromConnectionString(connectionString, tokenCredential); + Channel grpcChannel = options.createGrpcChannel(); + + return new DurableTaskGrpcClientBuilder() + .grpcChannel(grpcChannel); + } + + /** + * Creates a DurableTaskGrpcClientBuilder that connects to Azure-managed Durable Task Scheduler. + * + * @param endpoint The endpoint for connecting to Azure-managed Durable Task Scheduler. + * @param taskHubName The task hub name for connecting to Azure-managed Durable Task Scheduler. + * @param tokenCredential The token credential for connecting to Azure-managed Durable Task Scheduler. + * @return A new DurableTaskGrpcClientBuilder instance. + */ + public static DurableTaskGrpcClientBuilder createClientBuilder(String endpoint, String taskHubName, @Nullable TokenCredential tokenCredential) { + DurableTaskSchedulerClientOptions options = new DurableTaskSchedulerClientOptions() + .setEndpoint(endpoint) + .setTaskHubName(taskHubName) + .setTokenCredential(tokenCredential); + + Channel grpcChannel = options.createGrpcChannel(); + + return new DurableTaskGrpcClientBuilder() + .grpcChannel(grpcChannel); + } } \ No newline at end of file diff --git a/azuremanaged/client/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientOptions.java b/azuremanaged/client/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientOptions.java index 629f6995..579063ef 100644 --- a/azuremanaged/client/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientOptions.java +++ b/azuremanaged/client/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientOptions.java @@ -201,7 +201,7 @@ public void validate() { Objects.requireNonNull(taskHubName, "taskHubName must not be null"); } - private static Channel createGrpcChannel() { + public Channel createGrpcChannel() { // Create token cache only if credential is not null AccessTokenCache tokenCache = null; if (credential != null) { From df4a0e416c98f0f9c4f794e7ae4c43fc886b1052 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 19 Mar 2025 08:52:33 -0700 Subject: [PATCH 30/53] client refactored --- .../DurableTaskSchedulerClientExtensions.java | 179 ++++++++++++------ 1 file changed, 116 insertions(+), 63 deletions(-) diff --git a/azuremanaged/client/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientExtensions.java b/azuremanaged/client/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientExtensions.java index 436067a5..f766789a 100644 --- a/azuremanaged/client/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientExtensions.java +++ b/azuremanaged/client/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientExtensions.java @@ -4,104 +4,157 @@ package com.microsoft.durabletask.client.azuremanaged; import com.azure.core.credential.TokenCredential; -import com.azure.core.credential.TokenRequestContext; import com.microsoft.durabletask.DurableTaskClient; import com.microsoft.durabletask.DurableTaskGrpcClientBuilder; -import com.microsoft.durabletask.shared.azuremanaged.AccessTokenCache; - import io.grpc.Channel; -import io.grpc.ClientCall; -import io.grpc.ClientInterceptor; -import io.grpc.ForwardingClientCall; -import io.grpc.ManagedChannelBuilder; -import io.grpc.Metadata; -import io.grpc.MethodDescriptor; -import io.grpc.CallOptions; - import java.util.Objects; +import javax.annotation.Nullable; /** * Extension methods for creating DurableTaskClient instances that connect to Azure-managed Durable Task Scheduler. + * This class provides various methods to create and configure clients using either connection strings or explicit parameters. */ -public class DurableTaskSchedulerClientExtensions { +public static class DurableTaskSchedulerClientExtensions { + /** + * Creates a DurableTaskClient using a connection string. + * + * @param connectionString The connection string for Azure-managed Durable Task Scheduler. + * @return A new DurableTaskClient instance. + */ + public static DurableTaskClient createClient(String connectionString) { + return createClient(connectionString, null); + } + /** - * Creates a DurableTaskClient that connects to Azure-managed Durable Task Scheduler using a connection string. + * Creates a DurableTaskClient using a connection string and token credential. * - * @param connectionString The connection string for connecting to Azure-managed Durable Task Scheduler. - * @param tokenCredential The token credential for connecting to Azure-managed Durable Task Scheduler. + * @param connectionString The connection string for Azure-managed Durable Task Scheduler. + * @param tokenCredential The token credential for authentication, or null to use connection string credentials. * @return A new DurableTaskClient instance. + * @throws NullPointerException if connectionString is null */ public static DurableTaskClient createClient(String connectionString, @Nullable TokenCredential tokenCredential) { Objects.requireNonNull(connectionString, "connectionString must not be null"); - DurableTaskSchedulerClientOptions options = DurableTaskSchedulerClientOptions.fromConnectionString(connectionString, tokenCredential); - Channel grpcChannel = options.createGrpcChannel(); - - return new DurableTaskGrpcClientBuilder() - .grpcChannel(grpcChannel) - .build(); + return createClientFromOptions( + DurableTaskSchedulerClientOptions.fromConnectionString(connectionString, tokenCredential)); } - public static DurableTaskClient createClient(String endpoint, String taskHubName, @Nullable TokenCredential tokenCredential) { - DurableTaskSchedulerClientOptions options = new DurableTaskSchedulerClientOptions() - .setEndpoint(endpoint) + /** + * Creates a DurableTaskClient using explicit endpoint and task hub parameters. + * + * @param endpoint The endpoint address for Azure-managed Durable Task Scheduler. + * @param taskHubName The name of the task hub to connect to. + * @param tokenCredential The token credential for authentication, or null for anonymous access. + * @return A new DurableTaskClient instance. + * @throws NullPointerException if endpoint or taskHubName is null + */ + public static DurableTaskClient createClient( + String endpoint, + String taskHubName, + @Nullable TokenCredential tokenCredential) { + Objects.requireNonNull(endpoint, "endpoint must not be null"); + Objects.requireNonNull(taskHubName, "taskHubName must not be null"); + + return createClientFromOptions(new DurableTaskSchedulerClientOptions() + .setEndpointAddress(endpoint) .setTaskHubName(taskHubName) - .setTokenCredential(tokenCredential); - - Channel grpcChannel = options.createGrpcChannel(); + .setCredential(tokenCredential)); + } - return new DurableTaskGrpcClientBuilder() - .grpcChannel(grpcChannel) - .build(); + /** + * Configures a DurableTaskGrpcClientBuilder to use Azure-managed Durable Task Scheduler with a connection string. + * + * @param builder The builder to configure. + * @param connectionString The connection string for Azure-managed Durable Task Scheduler. + * @param tokenCredential The token credential for authentication, or null to use connection string credentials. + * @throws NullPointerException if builder or connectionString is null + */ + public static void useDurableTaskScheduler( + DurableTaskGrpcClientBuilder builder, + String connectionString, + @Nullable TokenCredential tokenCredential) { + Objects.requireNonNull(builder, "builder must not be null"); + Objects.requireNonNull(connectionString, "connectionString must not be null"); + + configureBuilder(builder, + DurableTaskSchedulerClientOptions.fromConnectionString(connectionString, tokenCredential)); } - public static void UseDurableTaskScheduler(DurableTaskGrpcClientBuilder builder, String endpoint, String taskHubName, @Nullable TokenCredential tokenCredential) { - DurableTaskSchedulerClientOptions options = new DurableTaskSchedulerClientOptions() - .setEndpoint(endpoint) + /** + * Configures a DurableTaskGrpcClientBuilder to use Azure-managed Durable Task Scheduler with explicit parameters. + * + * @param builder The builder to configure. + * @param endpoint The endpoint address for Azure-managed Durable Task Scheduler. + * @param taskHubName The name of the task hub to connect to. + * @param tokenCredential The token credential for authentication, or null for anonymous access. + * @throws NullPointerException if builder, endpoint, or taskHubName is null + */ + public static void useDurableTaskScheduler( + DurableTaskGrpcClientBuilder builder, + String endpoint, + String taskHubName, + @Nullable TokenCredential tokenCredential) { + Objects.requireNonNull(builder, "builder must not be null"); + Objects.requireNonNull(endpoint, "endpoint must not be null"); + Objects.requireNonNull(taskHubName, "taskHubName must not be null"); + + configureBuilder(builder, new DurableTaskSchedulerClientOptions() + .setEndpointAddress(endpoint) .setTaskHubName(taskHubName) - .setTokenCredential(tokenCredential); - - Channel grpcChannel = options.createGrpcChannel(); - builder.grpcChannel(grpcChannel); - } - public static void UseDurableTaskScheduler(DurableTaskGrpcClientBuilder builder, String connectionString, @Nullable TokenCredential tokenCredential) { - DurableTaskSchedulerClientOptions options = DurableTaskSchedulerClientOptions.fromConnectionString(connectionString, tokenCredential); - Channel grpcChannel = options.createGrpcChannel(); - builder.grpcChannel(grpcChannel); + .setCredential(tokenCredential)); } /** - * Creates a DurableTaskGrpcClientBuilder that connects to Azure-managed Durable Task Scheduler using a connection string. + * Creates a DurableTaskGrpcClientBuilder configured for Azure-managed Durable Task Scheduler using a connection string. * - * @param connectionString The connection string for connecting to Azure-managed Durable Task Scheduler. - * @param tokenCredential The token credential for connecting to Azure-managed Durable Task Scheduler. - * @return A new DurableTaskGrpcClientBuilder instance. + * @param connectionString The connection string for Azure-managed Durable Task Scheduler. + * @param tokenCredential The token credential for authentication, or null to use connection string credentials. + * @return A new configured DurableTaskGrpcClientBuilder instance. + * @throws NullPointerException if connectionString is null */ - public static DurableTaskGrpcClientBuilder createClientBuilder(String connectionString, @Nullable TokenCredential tokenCredential) { + public static DurableTaskGrpcClientBuilder createClientBuilder( + String connectionString, + @Nullable TokenCredential tokenCredential) { Objects.requireNonNull(connectionString, "connectionString must not be null"); - DurableTaskSchedulerClientOptions options = DurableTaskSchedulerClientOptions.fromConnectionString(connectionString, tokenCredential); - Channel grpcChannel = options.createGrpcChannel(); - - return new DurableTaskGrpcClientBuilder() - .grpcChannel(grpcChannel); + return createBuilderFromOptions( + DurableTaskSchedulerClientOptions.fromConnectionString(connectionString, tokenCredential)); } /** - * Creates a DurableTaskGrpcClientBuilder that connects to Azure-managed Durable Task Scheduler. + * Creates a DurableTaskGrpcClientBuilder configured for Azure-managed Durable Task Scheduler using explicit parameters. * - * @param endpoint The endpoint for connecting to Azure-managed Durable Task Scheduler. - * @param taskHubName The task hub name for connecting to Azure-managed Durable Task Scheduler. - * @param tokenCredential The token credential for connecting to Azure-managed Durable Task Scheduler. - * @return A new DurableTaskGrpcClientBuilder instance. + * @param endpoint The endpoint address for Azure-managed Durable Task Scheduler. + * @param taskHubName The name of the task hub to connect to. + * @param tokenCredential The token credential for authentication, or null for anonymous access. + * @return A new configured DurableTaskGrpcClientBuilder instance. + * @throws NullPointerException if endpoint or taskHubName is null */ - public static DurableTaskGrpcClientBuilder createClientBuilder(String endpoint, String taskHubName, @Nullable TokenCredential tokenCredential) { - DurableTaskSchedulerClientOptions options = new DurableTaskSchedulerClientOptions() - .setEndpoint(endpoint) + public static DurableTaskGrpcClientBuilder createClientBuilder( + String endpoint, + String taskHubName, + @Nullable TokenCredential tokenCredential) { + Objects.requireNonNull(endpoint, "endpoint must not be null"); + Objects.requireNonNull(taskHubName, "taskHubName must not be null"); + + return createBuilderFromOptions(new DurableTaskSchedulerClientOptions() + .setEndpointAddress(endpoint) .setTaskHubName(taskHubName) - .setTokenCredential(tokenCredential); + .setCredential(tokenCredential)); + } + // Private helper methods to reduce code duplication + + private static DurableTaskClient createClientFromOptions(DurableTaskSchedulerClientOptions options) { + return createBuilderFromOptions(options).build(); + } + + private static DurableTaskGrpcClientBuilder createBuilderFromOptions(DurableTaskSchedulerClientOptions options) { Channel grpcChannel = options.createGrpcChannel(); + return new DurableTaskGrpcClientBuilder().grpcChannel(grpcChannel); + } - return new DurableTaskGrpcClientBuilder() - .grpcChannel(grpcChannel); + private static void configureBuilder(DurableTaskGrpcClientBuilder builder, DurableTaskSchedulerClientOptions options) { + Channel grpcChannel = options.createGrpcChannel(); + builder.grpcChannel(grpcChannel); } } \ No newline at end of file From 0f79297b4c8f49e67da42b8ad503e3ca7ba95a27 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 19 Mar 2025 09:05:16 -0700 Subject: [PATCH 31/53] worker done --- .../DurableTaskSchedulerWorkerExtensions.java | 197 ++++++++++++------ .../DurableTaskSchedulerWorkerOptions.java | 192 +++++++++++++---- 2 files changed, 280 insertions(+), 109 deletions(-) diff --git a/azuremanaged/worker/src/main/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerExtensions.java b/azuremanaged/worker/src/main/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerExtensions.java index 990706e7..5cb6fc1d 100644 --- a/azuremanaged/worker/src/main/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerExtensions.java +++ b/azuremanaged/worker/src/main/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerExtensions.java @@ -19,84 +19,153 @@ import io.grpc.CallOptions; import java.util.Objects; +import javax.annotation.Nullable; /** - * Extension methods for creating DurableTaskGrpcWorker instances that connect to Azure-managed Durable Task Scheduler. + * Extension methods for creating DurableTaskWorker instances that connect to Azure-managed Durable Task Scheduler. + * This class provides various methods to create and configure workers using either connection strings or explicit parameters. */ -public class DurableTaskSchedulerWorkerExtensions { +public static class DurableTaskSchedulerWorkerExtensions { + /** + * Creates a DurableTaskWorker using a connection string. + * + * @param connectionString The connection string for Azure-managed Durable Task Scheduler. + * @return A new DurableTaskWorker instance. + */ + public static DurableTaskWorker createWorker(String connectionString) { + return createWorker(connectionString, null); + } /** - * Creates a DurableTaskGrpcWorkerBuilder that connects to Azure-managed Durable Task Scheduler. + * Creates a DurableTaskWorker using a connection string and token credential. * - * @param options The options for connecting to Azure-managed Durable Task Scheduler. - * @return A new DurableTaskGrpcWorkerBuilder instance. + * @param connectionString The connection string for Azure-managed Durable Task Scheduler. + * @param tokenCredential The token credential for authentication, or null to use connection string credentials. + * @return A new DurableTaskWorker instance. + * @throws NullPointerException if connectionString is null */ - public static DurableTaskGrpcWorkerBuilder createWorkerBuilder(DurableTaskSchedulerWorkerOptions options) { - Objects.requireNonNull(options, "options must not be null"); - options.validate(); + public static DurableTaskWorker createWorker(String connectionString, @Nullable TokenCredential tokenCredential) { + Objects.requireNonNull(connectionString, "connectionString must not be null"); + return createWorkerFromOptions( + DurableTaskSchedulerWorkerOptions.fromConnectionString(connectionString, tokenCredential)); + } - // Create the access token cache if credentials are provided - AccessTokenCache tokenCache = null; - TokenCredential credential = options.getTokenCredential(); - if (credential != null) { - TokenRequestContext context = new TokenRequestContext(); - context.addScopes(new String[] { options.getResourceId() + "/.default" }); - tokenCache = new AccessTokenCache(credential, context, options.getTokenRefreshMargin()); - } + /** + * Creates a DurableTaskWorker using explicit endpoint and task hub parameters. + * + * @param endpoint The endpoint address for Azure-managed Durable Task Scheduler. + * @param taskHubName The name of the task hub to connect to. + * @param tokenCredential The token credential for authentication, or null for anonymous access. + * @return A new DurableTaskWorker instance. + * @throws NullPointerException if endpoint or taskHubName is null + */ + public static DurableTaskWorker createWorker( + String endpoint, + String taskHubName, + @Nullable TokenCredential tokenCredential) { + Objects.requireNonNull(endpoint, "endpoint must not be null"); + Objects.requireNonNull(taskHubName, "taskHubName must not be null"); + + return createWorkerFromOptions(new DurableTaskSchedulerWorkerOptions() + .setEndpointAddress(endpoint) + .setTaskHubName(taskHubName) + .setCredential(tokenCredential)); + } - // Create the gRPC channel - Channel grpcChannel = createGrpcChannel(options, tokenCache); + /** + * Configures a DurableTaskGrpcWorkerBuilder to use Azure-managed Durable Task Scheduler with a connection string. + * + * @param builder The builder to configure. + * @param connectionString The connection string for Azure-managed Durable Task Scheduler. + * @param tokenCredential The token credential for authentication, or null to use connection string credentials. + * @throws NullPointerException if builder or connectionString is null + */ + public static void useDurableTaskScheduler( + DurableTaskGrpcWorkerBuilder builder, + String connectionString, + @Nullable TokenCredential tokenCredential) { + Objects.requireNonNull(builder, "builder must not be null"); + Objects.requireNonNull(connectionString, "connectionString must not be null"); + + configureBuilder(builder, + DurableTaskSchedulerWorkerOptions.fromConnectionString(connectionString, tokenCredential)); + } - // Create and return the worker builder - return new DurableTaskGrpcWorkerBuilder() - .grpcChannel(grpcChannel); + /** + * Configures a DurableTaskGrpcWorkerBuilder to use Azure-managed Durable Task Scheduler with explicit parameters. + * + * @param builder The builder to configure. + * @param endpoint The endpoint address for Azure-managed Durable Task Scheduler. + * @param taskHubName The name of the task hub to connect to. + * @param tokenCredential The token credential for authentication, or null for anonymous access. + * @throws NullPointerException if builder, endpoint, or taskHubName is null + */ + public static void useDurableTaskScheduler( + DurableTaskGrpcWorkerBuilder builder, + String endpoint, + String taskHubName, + @Nullable TokenCredential tokenCredential) { + Objects.requireNonNull(builder, "builder must not be null"); + Objects.requireNonNull(endpoint, "endpoint must not be null"); + Objects.requireNonNull(taskHubName, "taskHubName must not be null"); + + configureBuilder(builder, new DurableTaskSchedulerWorkerOptions() + .setEndpointAddress(endpoint) + .setTaskHubName(taskHubName) + .setCredential(tokenCredential)); } - private static Channel createGrpcChannel(DurableTaskSchedulerWorkerOptions options, AccessTokenCache tokenCache) { - // Normalize the endpoint URL and add DNS scheme for gRPC name resolution - String endpoint = "dns:///" + options.getEndpoint(); + /** + * Creates a DurableTaskGrpcWorkerBuilder configured for Azure-managed Durable Task Scheduler using a connection string. + * + * @param connectionString The connection string for Azure-managed Durable Task Scheduler. + * @param tokenCredential The token credential for authentication, or null to use connection string credentials. + * @return A new configured DurableTaskGrpcWorkerBuilder instance. + * @throws NullPointerException if connectionString is null + */ + public static DurableTaskGrpcWorkerBuilder createWorkerBuilder( + String connectionString, + @Nullable TokenCredential tokenCredential) { + Objects.requireNonNull(connectionString, "connectionString must not be null"); + return createBuilderFromOptions( + DurableTaskSchedulerWorkerOptions.fromConnectionString(connectionString, tokenCredential)); + } - // Create metadata interceptor to add task hub name and auth token - ClientInterceptor metadataInterceptor = new ClientInterceptor() { - @Override - public ClientCall interceptCall( - MethodDescriptor method, - CallOptions callOptions, - Channel next) { - return new ForwardingClientCall.SimpleForwardingClientCall( - next.newCall(method, callOptions)) { - @Override - public void start(ClientCall.Listener responseListener, Metadata headers) { - headers.put( - Metadata.Key.of("taskhub", Metadata.ASCII_STRING_MARSHALLER), - options.getTaskHubName() - ); - - // Add authorization token if credentials are configured - if (tokenCache != null) { - String token = tokenCache.getToken().getToken(); - headers.put( - Metadata.Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER), - "Bearer " + token - ); - } - - super.start(responseListener, headers); - } - }; - } - }; + /** + * Creates a DurableTaskGrpcWorkerBuilder configured for Azure-managed Durable Task Scheduler using explicit parameters. + * + * @param endpoint The endpoint address for Azure-managed Durable Task Scheduler. + * @param taskHubName The name of the task hub to connect to. + * @param tokenCredential The token credential for authentication, or null for anonymous access. + * @return A new configured DurableTaskGrpcWorkerBuilder instance. + * @throws NullPointerException if endpoint or taskHubName is null + */ + public static DurableTaskGrpcWorkerBuilder createWorkerBuilder( + String endpoint, + String taskHubName, + @Nullable TokenCredential tokenCredential) { + Objects.requireNonNull(endpoint, "endpoint must not be null"); + Objects.requireNonNull(taskHubName, "taskHubName must not be null"); - // Build the channel with appropriate security settings - ManagedChannelBuilder builder = ManagedChannelBuilder.forTarget(endpoint) - .intercept(metadataInterceptor); - - if (!options.isAllowInsecure()) { - builder.useTransportSecurity(); - } else { - builder.usePlaintext(); - } + return createBuilderFromOptions(new DurableTaskSchedulerWorkerOptions() + .setEndpointAddress(endpoint) + .setTaskHubName(taskHubName) + .setCredential(tokenCredential)); + } + + // Private helper methods to reduce code duplication + + private static DurableTaskWorker createWorkerFromOptions(DurableTaskSchedulerWorkerOptions options) { + return createBuilderFromOptions(options).build(); + } + + private static DurableTaskGrpcWorkerBuilder createBuilderFromOptions(DurableTaskSchedulerWorkerOptions options) { + Channel grpcChannel = options.createGrpcChannel(); + return new DurableTaskGrpcWorkerBuilder().grpcChannel(grpcChannel); + } - return builder.build(); + private static void configureBuilder(DurableTaskGrpcWorkerBuilder builder, DurableTaskSchedulerWorkerOptions options) { + Channel grpcChannel = options.createGrpcChannel(); + builder.grpcChannel(grpcChannel); } } \ No newline at end of file diff --git a/azuremanaged/worker/src/main/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerOptions.java b/azuremanaged/worker/src/main/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerOptions.java index 43f09360..6a7680af 100644 --- a/azuremanaged/worker/src/main/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerOptions.java +++ b/azuremanaged/worker/src/main/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerOptions.java @@ -5,19 +5,38 @@ import com.azure.core.credential.TokenCredential; import com.microsoft.durabletask.shared.azuremanaged.DurableTaskSchedulerConnectionString; +import io.grpc.Channel; +import io.grpc.ChannelCredentials; +import io.grpc.Grpc; +import io.grpc.InsecureChannelCredentials; +import io.grpc.TlsChannelCredentials; +import io.grpc.ClientInterceptor; +import io.grpc.ClientCall; +import io.grpc.Metadata; +import io.grpc.MethodDescriptor; +import io.grpc.CallOptions; +import io.grpc.ForwardingClientCall; +import jakarta.validation.constraints.NotBlank; import java.time.Duration; import java.util.Objects; +import java.net.URL; +import java.net.MalformedURLException; +import javax.annotation.Nullable; /** - * Configuration options for connecting to Azure-managed Durable Task Scheduler as a worker. + * Options for configuring the Durable Task Scheduler worker. */ public class DurableTaskSchedulerWorkerOptions { - private String endpoint; - private String taskHubName; + @NotBlank(message = "Endpoint address is required") + private String endpointAddress = ""; + + @NotBlank(message = "Task hub name is required") + private String taskHubName = ""; + + private TokenCredential credential; private String resourceId = "https://durabletask.io"; - private boolean allowInsecure = false; - private TokenCredential tokenCredential; + private boolean allowInsecureCredentials = false; private Duration tokenRefreshMargin = Duration.ofMinutes(5); /** @@ -30,37 +49,47 @@ public DurableTaskSchedulerWorkerOptions() { * Creates a new instance of DurableTaskSchedulerWorkerOptions from a connection string. * * @param connectionString The connection string to parse. + * @param credential The token credential for authentication, or null to use connection string credentials. * @return A new DurableTaskSchedulerWorkerOptions object. */ - public static DurableTaskSchedulerWorkerOptions fromConnectionString(String connectionString) { + public static DurableTaskSchedulerWorkerOptions fromConnectionString(String connectionString, @Nullable TokenCredential credential) { DurableTaskSchedulerConnectionString parsedConnectionString = DurableTaskSchedulerConnectionString.parse(connectionString); - + return fromConnectionString(parsedConnectionString, credential); + } + + /** + * Creates a new instance of DurableTaskSchedulerWorkerOptions from a parsed connection string. + * + * @param connectionString The parsed connection string. + * @param credential The token credential for authentication, or null to use connection string credentials. + * @return A new DurableTaskSchedulerWorkerOptions object. + */ + static DurableTaskSchedulerWorkerOptions fromConnectionString(DurableTaskSchedulerConnectionString connectionString, @Nullable TokenCredential credential) { DurableTaskSchedulerWorkerOptions options = new DurableTaskSchedulerWorkerOptions(); - options.setEndpoint(parsedConnectionString.getEndpoint()); - options.setTaskHubName(parsedConnectionString.getTaskHubName()); - options.setResourceId(parsedConnectionString.getResourceId()); - options.setAllowInsecure(parsedConnectionString.isAllowInsecure()); - + options.setEndpointAddress(connectionString.getEndpoint()); + options.setTaskHubName(connectionString.getTaskHubName()); + options.setCredential(credential); + options.setAllowInsecureCredentials(credential == null); return options; } /** - * Gets the endpoint URL. + * Gets the endpoint address. * - * @return The endpoint URL. + * @return The endpoint address. */ - public String getEndpoint() { - return endpoint; + public String getEndpointAddress() { + return endpointAddress; } /** - * Sets the endpoint URL. + * Sets the endpoint address. * - * @param endpoint The endpoint URL. + * @param endpointAddress The endpoint address. * @return This options object. */ - public DurableTaskSchedulerWorkerOptions setEndpoint(String endpoint) { - this.endpoint = endpoint; + public DurableTaskSchedulerWorkerOptions setEndpointAddress(String endpointAddress) { + this.endpointAddress = endpointAddress; return this; } @@ -85,62 +114,62 @@ public DurableTaskSchedulerWorkerOptions setTaskHubName(String taskHubName) { } /** - * Gets the resource ID for authentication. + * Gets the credential used for authentication. * - * @return The resource ID. + * @return The credential. */ - public String getResourceId() { - return resourceId; + public TokenCredential getCredential() { + return credential; } /** - * Sets the resource ID for authentication. + * Sets the credential used for authentication. * - * @param resourceId The resource ID. + * @param credential The credential. * @return This options object. */ - public DurableTaskSchedulerWorkerOptions setResourceId(String resourceId) { - this.resourceId = resourceId; + public DurableTaskSchedulerWorkerOptions setCredential(TokenCredential credential) { + this.credential = credential; return this; } /** - * Gets whether insecure connections are allowed. + * Gets the resource ID. * - * @return True if insecure connections are allowed, false otherwise. + * @return The resource ID. */ - public boolean isAllowInsecure() { - return allowInsecure; + public String getResourceId() { + return resourceId; } /** - * Sets whether insecure connections are allowed. + * Sets the resource ID. * - * @param allowInsecure True to allow insecure connections, false otherwise. + * @param resourceId The resource ID. * @return This options object. */ - public DurableTaskSchedulerWorkerOptions setAllowInsecure(boolean allowInsecure) { - this.allowInsecure = allowInsecure; + public DurableTaskSchedulerWorkerOptions setResourceId(String resourceId) { + this.resourceId = resourceId; return this; } /** - * Gets the token credential for authentication. + * Gets whether insecure credentials are allowed. * - * @return The token credential. + * @return True if insecure credentials are allowed. */ - public TokenCredential getTokenCredential() { - return tokenCredential; + public boolean isAllowInsecureCredentials() { + return allowInsecureCredentials; } /** - * Sets the token credential for authentication. + * Sets whether insecure credentials are allowed. * - * @param tokenCredential The token credential. + * @param allowInsecureCredentials True to allow insecure credentials. * @return This options object. */ - public DurableTaskSchedulerWorkerOptions setTokenCredential(TokenCredential tokenCredential) { - this.tokenCredential = tokenCredential; + public DurableTaskSchedulerWorkerOptions setAllowInsecureCredentials(boolean allowInsecureCredentials) { + this.allowInsecureCredentials = allowInsecureCredentials; return this; } @@ -170,7 +199,80 @@ public DurableTaskSchedulerWorkerOptions setTokenRefreshMargin(Duration tokenRef * @throws IllegalArgumentException If the options are not properly configured. */ public void validate() { - Objects.requireNonNull(endpoint, "endpoint must not be null"); + Objects.requireNonNull(endpointAddress, "endpointAddress must not be null"); Objects.requireNonNull(taskHubName, "taskHubName must not be null"); } + + /** + * Creates a gRPC channel using the configured options. + * + * @return A configured gRPC channel. + * @throws MalformedURLException If the endpoint address is invalid. + */ + public Channel createGrpcChannel() throws MalformedURLException { + // Create token cache only if credential is not null + AccessTokenCache tokenCache = null; + if (credential != null) { + TokenRequestContext context = new TokenRequestContext(); + context.addScopes(new String[] { this.resourceId + "/.default" }); + tokenCache = new AccessTokenCache(this.credential, context, this.tokenRefreshMargin); + } + + // Parse and normalize the endpoint URL + String endpoint = endpointAddress; + // Add https:// prefix if no protocol is specified + if (!endpoint.startsWith("http://") && !endpoint.startsWith("https://")) { + endpoint = "https://" + endpoint; + } + + URL url = new URL(endpoint); + String authority = url.getHost(); + if (url.getPort() != -1) { + authority += ":" + url.getPort(); + } + + // Create metadata interceptor to add task hub name and auth token + ClientInterceptor metadataInterceptor = new ClientInterceptor() { + @Override + public ClientCall interceptCall( + MethodDescriptor method, + CallOptions callOptions, + Channel next) { + return new ForwardingClientCall.SimpleForwardingClientCall( + next.newCall(method, callOptions)) { + @Override + public void start(ClientCall.Listener responseListener, Metadata headers) { + headers.put( + Metadata.Key.of("taskhub", Metadata.ASCII_STRING_MARSHALLER), + taskHubName + ); + + // Add authorization token if credentials are configured + if (tokenCache != null) { + String token = tokenCache.getToken().getToken(); + headers.put( + Metadata.Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER), + "Bearer " + token + ); + } + + super.start(responseListener, headers); + } + }; + } + }; + + // Configure channel credentials based on endpoint protocol + ChannelCredentials credentials; + if (endpoint.toLowerCase().startsWith("https://")) { + credentials = TlsChannelCredentials.create(); + } else { + credentials = InsecureChannelCredentials.create(); + } + + // Create channel with credentials + return Grpc.newChannelBuilder(authority, credentials) + .intercept(metadataInterceptor) + .build(); + } } \ No newline at end of file From c7eb4f1dafa06b7ae7845e953b478aa0c82cfbf3 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 19 Mar 2025 09:16:11 -0700 Subject: [PATCH 32/53] sample update --- .../DurableTaskSchedulerWorkerExtensions.java | 110 ----------- .../WebAppToDurableTaskSchedulerSample.java | 183 ++++++------------ 2 files changed, 55 insertions(+), 238 deletions(-) delete mode 100644 azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/worker/DurableTaskSchedulerWorkerExtensions.java diff --git a/azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/worker/DurableTaskSchedulerWorkerExtensions.java b/azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/worker/DurableTaskSchedulerWorkerExtensions.java deleted file mode 100644 index dc377144..00000000 --- a/azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/worker/DurableTaskSchedulerWorkerExtensions.java +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package com.microsoft.durabletask.azuremanaged.worker; - -import com.azure.core.credential.TokenCredential; -import com.azure.core.credential.TokenRequestContext; -import com.microsoft.durabletask.DurableTaskGrpcWorker; -import com.microsoft.durabletask.DurableTaskGrpcWorkerBuilder; -import com.microsoft.durabletask.azuremanaged.shared.AccessTokenCache; -import com.microsoft.durabletask.azuremanaged.shared.DurableTaskSchedulerConnectionString; -import io.grpc.Channel; -import io.grpc.ClientCall; -import io.grpc.ClientInterceptor; -import io.grpc.ForwardingClientCall; -import io.grpc.ManagedChannel; -import io.grpc.ManagedChannelBuilder; -import io.grpc.Metadata; -import io.grpc.MethodDescriptor; -import io.grpc.CallOptions; -import io.grpc.netty.GrpcSslContexts; -import io.grpc.netty.NettyChannelBuilder; -import io.netty.handler.ssl.SslContext; -import io.netty.handler.ssl.SslContextBuilder; -import io.netty.handler.ssl.util.InsecureTrustManagerFactory; - -import javax.net.ssl.SSLException; -import java.time.Duration; -import java.util.Objects; - -/** - * Extension methods for creating DurableTaskGrpcWorker instances that connect to Azure-managed Durable Task Scheduler. - */ -public class DurableTaskSchedulerWorkerExtensions { - - /** - * Creates a DurableTaskGrpcWorkerBuilder that connects to Azure-managed Durable Task Scheduler. - * - * @param options The options for connecting to Azure-managed Durable Task Scheduler. - * @return A new DurableTaskGrpcWorkerBuilder instance. - */ - public static DurableTaskGrpcWorkerBuilder createWorkerBuilder(DurableTaskSchedulerWorkerOptions options) { - Objects.requireNonNull(options, "options must not be null"); - options.validate(); - - // Create the access token cache if credentials are provided - AccessTokenCache tokenCache = null; - TokenCredential credential = options.getTokenCredential(); - if (credential != null) { - TokenRequestContext context = new TokenRequestContext(); - context.addScopes(new String[] { options.getResourceId() + "/.default" }); - tokenCache = new AccessTokenCache(credential, context, options.getTokenRefreshMargin()); - } - - // Create the gRPC channel - Channel grpcChannel = createGrpcChannel(options, tokenCache); - - // Create and return the worker builder - return new DurableTaskGrpcWorkerBuilder() - .grpcChannel(grpcChannel); - } - - static Channel createGrpcChannel(DurableTaskSchedulerWorkerOptions options, AccessTokenCache tokenCache) { - // Normalize the endpoint URL and add DNS scheme for gRPC name resolution - String endpoint = "dns:///" + options.getEndpoint(); - - // Create metadata interceptor to add task hub name and auth token - ClientInterceptor metadataInterceptor = new ClientInterceptor() { - @Override - public ClientCall interceptCall( - MethodDescriptor method, - CallOptions callOptions, - Channel next) { - return new ForwardingClientCall.SimpleForwardingClientCall( - next.newCall(method, callOptions)) { - @Override - public void start(ClientCall.Listener responseListener, Metadata headers) { - headers.put( - Metadata.Key.of("taskhub", Metadata.ASCII_STRING_MARSHALLER), - options.getTaskHubName() - ); - - // Add authorization token if credentials are configured - if (tokenCache != null) { - String token = tokenCache.getToken().getToken(); - headers.put( - Metadata.Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER), - "Bearer " + token - ); - } - - super.start(responseListener, headers); - } - }; - } - }; - - // Build the channel with appropriate security settings - ManagedChannelBuilder builder = ManagedChannelBuilder.forTarget(endpoint) - .intercept(metadataInterceptor); - - if (!options.isAllowInsecure()) { - builder.useTransportSecurity(); - } else { - builder.usePlaintext(); - } - - return builder.build(); - } -} \ No newline at end of file diff --git a/samples/src/main/java/io/durabletask/samples/WebAppToDurableTaskSchedulerSample.java b/samples/src/main/java/io/durabletask/samples/WebAppToDurableTaskSchedulerSample.java index 4f2c3ee2..f11a410a 100644 --- a/samples/src/main/java/io/durabletask/samples/WebAppToDurableTaskSchedulerSample.java +++ b/samples/src/main/java/io/durabletask/samples/WebAppToDurableTaskSchedulerSample.java @@ -2,46 +2,37 @@ // Licensed under the MIT License. package io.durabletask.samples; -import com.azure.core.credential.AccessToken; +import com.azure.core.credential.TokenCredential; import com.microsoft.durabletask.*; import com.microsoft.durabletask.client.azuremanaged.DurableTaskSchedulerClientExtensions; import com.microsoft.durabletask.client.azuremanaged.DurableTaskSchedulerClientOptions; import com.microsoft.durabletask.worker.azuremanaged.DurableTaskSchedulerWorkerExtensions; import com.microsoft.durabletask.worker.azuremanaged.DurableTaskSchedulerWorkerOptions; -import com.microsoft.durabletask.shared.azuremanaged.AccessTokenCache; -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.*; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.beans.factory.annotation.Value; import java.time.Duration; -import io.grpc.Channel; -import java.util.Objects; import com.azure.identity.DefaultAzureCredentialBuilder; -import com.azure.core.credential.TokenCredential; -import com.azure.core.credential.TokenRequestContext; @ConfigurationProperties(prefix = "durable.task") @lombok.Data class DurableTaskProperties { private String endpoint; - private String hubName; + private String taskHubName; private String resourceId = "https://durabletask.io"; - private boolean allowInsecure = false; + private boolean allowInsecureCredentials = false; } /** - * Sample Spring Boot application demonstrating Azure Durable Task integration. + * Sample Spring Boot application demonstrating Azure-managed Durable Task integration. * This sample shows how to: - * 1. Configure Durable Task with Spring Boot + * 1. Configure Azure-managed Durable Task with Spring Boot * 2. Create orchestrations and activities * 3. Handle REST API endpoints for order processing */ @@ -65,120 +56,67 @@ public TokenCredential tokenCredential() { return new DefaultAzureCredentialBuilder().build(); } - @Bean - public AccessTokenCache accessTokenCache( - TokenCredential credential, - DurableTaskProperties properties) { - if (credential == null) { - return null; - } - TokenRequestContext context = new TokenRequestContext(); - context.addScopes(new String[] { properties.getResourceId() + "/.default" }); - return new AccessTokenCache( - credential, context, Duration.ofMinutes(5) - ); - } - @Bean public DurableTaskGrpcWorker durableTaskWorker( DurableTaskProperties properties, TokenCredential tokenCredential) { - // Create worker options - DurableTaskSchedulerWorkerOptions options = new DurableTaskSchedulerWorkerOptions() - .setEndpoint(properties.getEndpoint()) - .setTaskHubName(properties.getHubName()) - .setResourceId(properties.getResourceId()) - .setAllowInsecure(properties.isAllowInsecure()) - .setTokenCredential(tokenCredential); - - // Create worker builder - DurableTaskGrpcWorkerBuilder builder = DurableTaskSchedulerWorkerExtensions.createWorkerBuilder(options); - + // Create worker using Azure-managed extensions + DurableTaskGrpcWorkerBuilder workerBuilder = DurableTaskSchedulerWorkerExtensions.createWorkerBuilder( + properties.getEndpoint(), + properties.getTaskHubName(), + tokenCredential); + // Add orchestrations - builder.addOrchestration(new TaskOrchestrationFactory() { - @Override - public String getName() { - return "ProcessOrderOrchestration"; + workerBuilder.addOrchestration("ProcessOrderOrchestration", ctx -> { + // Get the order input as JSON string + String orderJson = ctx.getInput(String.class); + + // Process the order through multiple activities + boolean isValid = ctx.callActivity("ValidateOrder", orderJson, Boolean.class).await(); + if (!isValid) { + ctx.complete("{\"status\": \"FAILED\", \"message\": \"Order validation failed\"}"); + return; } - @Override - public TaskOrchestration create() { - return ctx -> { - // Get the order input as JSON string - String orderJson = ctx.getInput(String.class); - - // Process the order through multiple activities - boolean isValid = ctx.callActivity("ValidateOrder", orderJson, Boolean.class).await(); - if (!isValid) { - ctx.complete("{\"status\": \"FAILED\", \"message\": \"Order validation failed\"}"); - return; - } - - // Process payment - String paymentResult = ctx.callActivity("ProcessPayment", orderJson, String.class).await(); - if (!paymentResult.contains("\"success\":true")) { - ctx.complete("{\"status\": \"FAILED\", \"message\": \"Payment processing failed\"}"); - return; - } - - // Ship order - String shipmentResult = ctx.callActivity("ShipOrder", orderJson, String.class).await(); - - // Return the final result - ctx.complete("{\"status\": \"SUCCESS\", " + - "\"payment\": " + paymentResult + ", " + - "\"shipment\": " + shipmentResult + "}"); - }; + // Process payment + String paymentResult = ctx.callActivity("ProcessPayment", orderJson, String.class).await(); + if (!paymentResult.contains("\"success\":true")) { + ctx.complete("{\"status\": \"FAILED\", \"message\": \"Payment processing failed\"}"); + return; } + + // Ship order + String shipmentResult = ctx.callActivity("ShipOrder", orderJson, String.class).await(); + + // Return the final result + ctx.complete("{\"status\": \"SUCCESS\", " + + "\"payment\": " + paymentResult + ", " + + "\"shipment\": " + shipmentResult + "}"); }); // Add activity implementations - builder.addActivity(new TaskActivityFactory() { - @Override - public String getName() { return "ValidateOrder"; } - - @Override - public TaskActivity create() { - return ctx -> { - String orderJson = ctx.getInput(String.class); - // Simple validation - check if order contains amount and it's greater than 0 - return orderJson.contains("\"amount\"") && !orderJson.contains("\"amount\":0"); - }; - } + workerBuilder.addActivity("ValidateOrder", ctx -> { + String orderJson = ctx.getInput(String.class); + // Simple validation - check if order contains amount and it's greater than 0 + return orderJson.contains("\"amount\"") && !orderJson.contains("\"amount\":0"); }); - builder.addActivity(new TaskActivityFactory() { - @Override - public String getName() { return "ProcessPayment"; } - - @Override - public TaskActivity create() { - return ctx -> { - String orderJson = ctx.getInput(String.class); - // Simulate payment processing - sleep(1000); // Simulate processing time - return "{\"success\":true, \"transactionId\":\"TXN" + System.currentTimeMillis() + "\"}"; - }; - } + workerBuilder.addActivity("ProcessPayment", ctx -> { + String orderJson = ctx.getInput(String.class); + // Simulate payment processing + sleep(1000); // Simulate processing time + return "{\"success\":true, \"transactionId\":\"TXN" + System.currentTimeMillis() + "\"}"; }); - builder.addActivity(new TaskActivityFactory() { - @Override - public String getName() { return "ShipOrder"; } - - @Override - public TaskActivity create() { - return ctx -> { - String orderJson = ctx.getInput(String.class); - // Simulate shipping process - sleep(1000); // Simulate processing time - return "{\"trackingNumber\":\"TRACK" + System.currentTimeMillis() + "\"}"; - }; - } + workerBuilder.addActivity("ShipOrder", ctx -> { + String orderJson = ctx.getInput(String.class); + // Simulate shipping process + sleep(1000); // Simulate processing time + return "{\"trackingNumber\":\"TRACK" + System.currentTimeMillis() + "\"}"; }); - return builder.build(); + return workerBuilder.build(); } @Bean @@ -186,16 +124,11 @@ public DurableTaskClient durableTaskClient( DurableTaskProperties properties, TokenCredential tokenCredential) { - // Create client options - DurableTaskSchedulerClientOptions options = new DurableTaskSchedulerClientOptions() - .setEndpoint(properties.getEndpoint()) - .setTaskHubName(properties.getHubName()) - .setResourceId(properties.getResourceId()) - .setAllowInsecure(properties.isAllowInsecure()) - .setTokenCredential(tokenCredential); - - // Create and return the client - return DurableTaskSchedulerClientExtensions.createClient(options); + // Create client using Azure-managed extensions + return DurableTaskSchedulerClientExtensions.createClient( + properties.getEndpoint(), + properties.getTaskHubName(), + tokenCredential); } } @@ -203,12 +136,11 @@ private static void sleep(int millis) { try { Thread.sleep(millis); } catch (InterruptedException e) { - // ignore + Thread.currentThread().interrupt(); } } } - /** * REST Controller for handling order-related operations. */ @@ -249,11 +181,6 @@ public String getOrder(@PathVariable String instanceId) throws Exception { if (metadata == null) { return "{\"error\": \"Order not found\"}"; } - - if (metadata.getRuntimeStatus() == OrchestrationRuntimeStatus.COMPLETED) { - return metadata.readOutputAs(String.class); - } else { - return "{\"status\": \"" + metadata.getRuntimeStatus() + "\"}"; - } + return metadata.readOutputAs(String.class); } } \ No newline at end of file From aa94d9cf8b4c141a53a3e00b9ed496accd03383a Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 19 Mar 2025 12:47:18 -0700 Subject: [PATCH 33/53] fix --- azuremanaged/client/build.gradle | 10 +-- .../DurableTaskSchedulerClientExtensions.java | 55 +--------------- .../DurableTaskSchedulerClientOptions.java | 45 ++++++------- azuremanaged/shared/build.gradle | 7 -- .../azuremanaged}/AccessTokenCache.java | 0 .../DurableTaskSchedulerConnectionString.java | 1 - azuremanaged/worker/build.gradle | 11 +--- .../DurableTaskSchedulerWorkerExtensions.java | 64 +------------------ .../DurableTaskSchedulerWorkerOptions.java | 23 ++++--- client/build.gradle | 6 -- .../durabletask/DurableTaskGrpcClient.java | 2 +- samples/build.gradle | 1 + .../WebAppToDurableTaskSchedulerSample.java | 8 +-- 13 files changed, 50 insertions(+), 183 deletions(-) rename azuremanaged/shared/{ => src/main/java/com/microsoft/durabletask/shared/azuremanaged}/AccessTokenCache.java (100%) rename azuremanaged/shared/{ => src/main/java/com/microsoft/durabletask/shared/azuremanaged}/DurableTaskSchedulerConnectionString.java (99%) diff --git a/azuremanaged/client/build.gradle b/azuremanaged/client/build.gradle index c69806e9..33141032 100644 --- a/azuremanaged/client/build.gradle +++ b/azuremanaged/client/build.gradle @@ -23,13 +23,13 @@ def protocVersion = '3.12.0' def jacksonVersion = '2.15.3' def azureCoreVersion = '1.45.0' def azureIdentityVersion = '1.11.1' +def durabletaskClientVersion = '1.5.0' // When build on local, you need to set this value to your local jdk11 directory. // Java11 is used to compile and run all the tests. def PATH_TO_TEST_JAVA_RUNTIME = System.env.JDK_11 ?: System.getProperty("java.home") repositories { mavenCentral() - jcenter() } dependencies { @@ -38,13 +38,7 @@ dependencies { implementation "io.grpc:grpc-stub:${grpcVersion}" runtimeOnly "io.grpc:grpc-netty-shaded:${grpcVersion}" - compileOnly "org.apache.tomcat:annotations-api:6.0.53" - - implementation "com.fasterxml.jackson.core:jackson-core:${jacksonVersion}" - implementation "com.fasterxml.jackson.core:jackson-databind:${jacksonVersion}" - implementation "com.fasterxml.jackson.core:jackson-annotations:${jacksonVersion}" - implementation "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:${jacksonVersion}" - + implementation "com.microsoft:durabletask-client:${durabletaskClientVersion}" implementation "com.azure:azure-core:${azureCoreVersion}" implementation "com.azure:azure-identity:${azureIdentityVersion}" diff --git a/azuremanaged/client/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientExtensions.java b/azuremanaged/client/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientExtensions.java index f766789a..ae784d63 100644 --- a/azuremanaged/client/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientExtensions.java +++ b/azuremanaged/client/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientExtensions.java @@ -3,9 +3,8 @@ package com.microsoft.durabletask.client.azuremanaged; -import com.azure.core.credential.TokenCredential; -import com.microsoft.durabletask.DurableTaskClient; import com.microsoft.durabletask.DurableTaskGrpcClientBuilder; +import com.azure.core.credential.TokenCredential; import io.grpc.Channel; import java.util.Objects; import javax.annotation.Nullable; @@ -14,52 +13,9 @@ * Extension methods for creating DurableTaskClient instances that connect to Azure-managed Durable Task Scheduler. * This class provides various methods to create and configure clients using either connection strings or explicit parameters. */ -public static class DurableTaskSchedulerClientExtensions { - /** - * Creates a DurableTaskClient using a connection string. - * - * @param connectionString The connection string for Azure-managed Durable Task Scheduler. - * @return A new DurableTaskClient instance. - */ - public static DurableTaskClient createClient(String connectionString) { - return createClient(connectionString, null); - } +public final class DurableTaskSchedulerClientExtensions { - /** - * Creates a DurableTaskClient using a connection string and token credential. - * - * @param connectionString The connection string for Azure-managed Durable Task Scheduler. - * @param tokenCredential The token credential for authentication, or null to use connection string credentials. - * @return A new DurableTaskClient instance. - * @throws NullPointerException if connectionString is null - */ - public static DurableTaskClient createClient(String connectionString, @Nullable TokenCredential tokenCredential) { - Objects.requireNonNull(connectionString, "connectionString must not be null"); - return createClientFromOptions( - DurableTaskSchedulerClientOptions.fromConnectionString(connectionString, tokenCredential)); - } - - /** - * Creates a DurableTaskClient using explicit endpoint and task hub parameters. - * - * @param endpoint The endpoint address for Azure-managed Durable Task Scheduler. - * @param taskHubName The name of the task hub to connect to. - * @param tokenCredential The token credential for authentication, or null for anonymous access. - * @return A new DurableTaskClient instance. - * @throws NullPointerException if endpoint or taskHubName is null - */ - public static DurableTaskClient createClient( - String endpoint, - String taskHubName, - @Nullable TokenCredential tokenCredential) { - Objects.requireNonNull(endpoint, "endpoint must not be null"); - Objects.requireNonNull(taskHubName, "taskHubName must not be null"); - - return createClientFromOptions(new DurableTaskSchedulerClientOptions() - .setEndpointAddress(endpoint) - .setTaskHubName(taskHubName) - .setCredential(tokenCredential)); - } + private DurableTaskSchedulerClientExtensions() {} /** * Configures a DurableTaskGrpcClientBuilder to use Azure-managed Durable Task Scheduler with a connection string. @@ -143,11 +99,6 @@ public static DurableTaskGrpcClientBuilder createClientBuilder( } // Private helper methods to reduce code duplication - - private static DurableTaskClient createClientFromOptions(DurableTaskSchedulerClientOptions options) { - return createBuilderFromOptions(options).build(); - } - private static DurableTaskGrpcClientBuilder createBuilderFromOptions(DurableTaskSchedulerClientOptions options) { Channel grpcChannel = options.createGrpcChannel(); return new DurableTaskGrpcClientBuilder().grpcChannel(grpcChannel); diff --git a/azuremanaged/client/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientOptions.java b/azuremanaged/client/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientOptions.java index 579063ef..d2a695d6 100644 --- a/azuremanaged/client/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientOptions.java +++ b/azuremanaged/client/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientOptions.java @@ -4,33 +4,21 @@ package com.microsoft.durabletask.client.azuremanaged; import com.azure.core.credential.TokenCredential; -import com.azure.identity.DefaultAzureCredential; -import com.azure.identity.ManagedIdentityCredential; -import com.azure.identity.WorkloadIdentityCredential; -import com.azure.identity.WorkloadIdentityCredentialOptions; -import com.azure.identity.EnvironmentCredential; -import com.azure.identity.AzureCliCredential; -import com.azure.identity.AzurePowerShellCredential; +import com.azure.core.credential.TokenRequestContext; import com.microsoft.durabletask.shared.azuremanaged.DurableTaskSchedulerConnectionString; -import io.grpc.ManagedChannel; -import io.grpc.ManagedChannelBuilder; -import io.grpc.Metadata; -import io.grpc.stub.MetadataUtils; - -import jakarta.validation.constraints.NotBlank; +import com.microsoft.durabletask.shared.azuremanaged.AccessTokenCache; +import io.grpc.*; +import javax.annotation.Nullable; +import java.net.MalformedURLException; import java.time.Duration; import java.util.Objects; import java.net.URL; -import java.net.MalformedURLException; /** * Options for configuring the Durable Task Scheduler. */ public class DurableTaskSchedulerClientOptions { - @NotBlank(message = "Endpoint address is required") private String endpointAddress = ""; - - @NotBlank(message = "Task hub name is required") private String taskHubName = ""; private TokenCredential credential; @@ -48,10 +36,11 @@ public DurableTaskSchedulerClientOptions() { * Creates a new instance of DurableTaskSchedulerClientOptions from a connection string. * * @param connectionString The connection string to parse. + * @param credential The credential to use for authentication. It is nullable for anonymous access. * @return A new DurableTaskSchedulerClientOptions object. */ public static DurableTaskSchedulerClientOptions fromConnectionString(String connectionString, @Nullable TokenCredential credential) { - DurableTaskSchedulerConnectionString parsedConnectionString = DurableTaskSchedulerConnectionString.parse(connectionString); + DurableTaskSchedulerConnectionString parsedConnectionString = new DurableTaskSchedulerConnectionString(connectionString); return fromConnectionString(parsedConnectionString, credential); } @@ -201,6 +190,11 @@ public void validate() { Objects.requireNonNull(taskHubName, "taskHubName must not be null"); } + /** + * Creates a gRPC channel using the configured options. + * + * @return A configured gRPC channel for communication with the Durable Task service. + */ public Channel createGrpcChannel() { // Create token cache only if credential is not null AccessTokenCache tokenCache = null; @@ -217,13 +211,20 @@ public Channel createGrpcChannel() { endpoint = "https://" + endpoint; } - URL url = new URL(endpoint); + URL url; + try { + url = new URL(endpoint); + } catch (MalformedURLException e) { + throw new IllegalArgumentException("Invalid endpoint URL: " + endpoint); + } + String authority = url.getHost(); if (url.getPort() != -1) { authority += ":" + url.getPort(); } // Create metadata interceptor to add task hub name and auth token + AccessTokenCache finalTokenCache = tokenCache; ClientInterceptor metadataInterceptor = new ClientInterceptor() { @Override public ClientCall interceptCall( @@ -240,8 +241,8 @@ public void start(ClientCall.Listener responseListener, Metadata headers) ); // Add authorization token if credentials are configured - if (tokenCache != null) { - String token = tokenCache.getToken().getToken(); + if (finalTokenCache != null) { + String token = finalTokenCache.getToken().getToken(); headers.put( Metadata.Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER), "Bearer " + token @@ -255,7 +256,7 @@ public void start(ClientCall.Listener responseListener, Metadata headers) }; ChannelCredentials credentials; - if (!this.allowInsecure) { + if (!this.allowInsecureCredentials) { credentials = io.grpc.TlsChannelCredentials.create(); } else { credentials = InsecureChannelCredentials.create(); diff --git a/azuremanaged/shared/build.gradle b/azuremanaged/shared/build.gradle index 81bb83b5..0fb13392 100644 --- a/azuremanaged/shared/build.gradle +++ b/azuremanaged/shared/build.gradle @@ -27,13 +27,6 @@ dependencies { implementation "io.grpc:grpc-stub:${grpcVersion}" runtimeOnly "io.grpc:grpc-netty-shaded:${grpcVersion}" - compileOnly "org.apache.tomcat:annotations-api:6.0.53" - - implementation "com.fasterxml.jackson.core:jackson-core:${jacksonVersion}" - implementation "com.fasterxml.jackson.core:jackson-databind:${jacksonVersion}" - implementation "com.fasterxml.jackson.core:jackson-annotations:${jacksonVersion}" - implementation "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:${jacksonVersion}" - implementation "com.azure:azure-core:${azureCoreVersion}" implementation "com.azure:azure-identity:${azureIdentityVersion}" diff --git a/azuremanaged/shared/AccessTokenCache.java b/azuremanaged/shared/src/main/java/com/microsoft/durabletask/shared/azuremanaged/AccessTokenCache.java similarity index 100% rename from azuremanaged/shared/AccessTokenCache.java rename to azuremanaged/shared/src/main/java/com/microsoft/durabletask/shared/azuremanaged/AccessTokenCache.java diff --git a/azuremanaged/shared/DurableTaskSchedulerConnectionString.java b/azuremanaged/shared/src/main/java/com/microsoft/durabletask/shared/azuremanaged/DurableTaskSchedulerConnectionString.java similarity index 99% rename from azuremanaged/shared/DurableTaskSchedulerConnectionString.java rename to azuremanaged/shared/src/main/java/com/microsoft/durabletask/shared/azuremanaged/DurableTaskSchedulerConnectionString.java index dd993590..816850af 100644 --- a/azuremanaged/shared/DurableTaskSchedulerConnectionString.java +++ b/azuremanaged/shared/src/main/java/com/microsoft/durabletask/shared/azuremanaged/DurableTaskSchedulerConnectionString.java @@ -7,7 +7,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Objects; /** * Represents the constituent parts of a connection string for a Durable Task Scheduler service. diff --git a/azuremanaged/worker/build.gradle b/azuremanaged/worker/build.gradle index 985a4efe..5e852a78 100644 --- a/azuremanaged/worker/build.gradle +++ b/azuremanaged/worker/build.gradle @@ -23,28 +23,21 @@ def protocVersion = '3.12.0' def jacksonVersion = '2.15.3' def azureCoreVersion = '1.45.0' def azureIdentityVersion = '1.11.1' +def durabletaskClientVersion = '1.5.0' // When build on local, you need to set this value to your local jdk11 directory. // Java11 is used to compile and run all the tests. def PATH_TO_TEST_JAVA_RUNTIME = System.env.JDK_11 ?: System.getProperty("java.home") repositories { mavenCentral() - jcenter() } dependencies { implementation project(':azuremanaged:shared') implementation "io.grpc:grpc-protobuf:${grpcVersion}" implementation "io.grpc:grpc-stub:${grpcVersion}" + implementation "com.microsoft:durabletask-client:${durabletaskClientVersion}" runtimeOnly "io.grpc:grpc-netty-shaded:${grpcVersion}" - - compileOnly "org.apache.tomcat:annotations-api:6.0.53" - - implementation "com.fasterxml.jackson.core:jackson-core:${jacksonVersion}" - implementation "com.fasterxml.jackson.core:jackson-databind:${jacksonVersion}" - implementation "com.fasterxml.jackson.core:jackson-annotations:${jacksonVersion}" - implementation "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:${jacksonVersion}" - implementation "com.azure:azure-core:${azureCoreVersion}" implementation "com.azure:azure-identity:${azureIdentityVersion}" diff --git a/azuremanaged/worker/src/main/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerExtensions.java b/azuremanaged/worker/src/main/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerExtensions.java index 5cb6fc1d..5a29c2a3 100644 --- a/azuremanaged/worker/src/main/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerExtensions.java +++ b/azuremanaged/worker/src/main/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerExtensions.java @@ -4,20 +4,9 @@ package com.microsoft.durabletask.worker.azuremanaged; import com.azure.core.credential.TokenCredential; -import com.azure.core.credential.TokenRequestContext; -import com.microsoft.durabletask.DurableTaskGrpcWorker; import com.microsoft.durabletask.DurableTaskGrpcWorkerBuilder; -import com.microsoft.durabletask.shared.azuremanaged.AccessTokenCache; import io.grpc.Channel; -import io.grpc.ClientCall; -import io.grpc.ClientInterceptor; -import io.grpc.ForwardingClientCall; -import io.grpc.ManagedChannelBuilder; -import io.grpc.Metadata; -import io.grpc.MethodDescriptor; -import io.grpc.CallOptions; - import java.util.Objects; import javax.annotation.Nullable; @@ -25,52 +14,8 @@ * Extension methods for creating DurableTaskWorker instances that connect to Azure-managed Durable Task Scheduler. * This class provides various methods to create and configure workers using either connection strings or explicit parameters. */ -public static class DurableTaskSchedulerWorkerExtensions { - /** - * Creates a DurableTaskWorker using a connection string. - * - * @param connectionString The connection string for Azure-managed Durable Task Scheduler. - * @return A new DurableTaskWorker instance. - */ - public static DurableTaskWorker createWorker(String connectionString) { - return createWorker(connectionString, null); - } - - /** - * Creates a DurableTaskWorker using a connection string and token credential. - * - * @param connectionString The connection string for Azure-managed Durable Task Scheduler. - * @param tokenCredential The token credential for authentication, or null to use connection string credentials. - * @return A new DurableTaskWorker instance. - * @throws NullPointerException if connectionString is null - */ - public static DurableTaskWorker createWorker(String connectionString, @Nullable TokenCredential tokenCredential) { - Objects.requireNonNull(connectionString, "connectionString must not be null"); - return createWorkerFromOptions( - DurableTaskSchedulerWorkerOptions.fromConnectionString(connectionString, tokenCredential)); - } - - /** - * Creates a DurableTaskWorker using explicit endpoint and task hub parameters. - * - * @param endpoint The endpoint address for Azure-managed Durable Task Scheduler. - * @param taskHubName The name of the task hub to connect to. - * @param tokenCredential The token credential for authentication, or null for anonymous access. - * @return A new DurableTaskWorker instance. - * @throws NullPointerException if endpoint or taskHubName is null - */ - public static DurableTaskWorker createWorker( - String endpoint, - String taskHubName, - @Nullable TokenCredential tokenCredential) { - Objects.requireNonNull(endpoint, "endpoint must not be null"); - Objects.requireNonNull(taskHubName, "taskHubName must not be null"); - - return createWorkerFromOptions(new DurableTaskSchedulerWorkerOptions() - .setEndpointAddress(endpoint) - .setTaskHubName(taskHubName) - .setCredential(tokenCredential)); - } +public final class DurableTaskSchedulerWorkerExtensions { + private DurableTaskSchedulerWorkerExtensions() {} /** * Configures a DurableTaskGrpcWorkerBuilder to use Azure-managed Durable Task Scheduler with a connection string. @@ -154,11 +99,6 @@ public static DurableTaskGrpcWorkerBuilder createWorkerBuilder( } // Private helper methods to reduce code duplication - - private static DurableTaskWorker createWorkerFromOptions(DurableTaskSchedulerWorkerOptions options) { - return createBuilderFromOptions(options).build(); - } - private static DurableTaskGrpcWorkerBuilder createBuilderFromOptions(DurableTaskSchedulerWorkerOptions options) { Channel grpcChannel = options.createGrpcChannel(); return new DurableTaskGrpcWorkerBuilder().grpcChannel(grpcChannel); diff --git a/azuremanaged/worker/src/main/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerOptions.java b/azuremanaged/worker/src/main/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerOptions.java index 6a7680af..04fa7a18 100644 --- a/azuremanaged/worker/src/main/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerOptions.java +++ b/azuremanaged/worker/src/main/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerOptions.java @@ -4,7 +4,9 @@ package com.microsoft.durabletask.worker.azuremanaged; import com.azure.core.credential.TokenCredential; +import com.azure.core.credential.TokenRequestContext; import com.microsoft.durabletask.shared.azuremanaged.DurableTaskSchedulerConnectionString; +import com.microsoft.durabletask.shared.azuremanaged.AccessTokenCache; import io.grpc.Channel; import io.grpc.ChannelCredentials; import io.grpc.Grpc; @@ -17,7 +19,6 @@ import io.grpc.CallOptions; import io.grpc.ForwardingClientCall; -import jakarta.validation.constraints.NotBlank; import java.time.Duration; import java.util.Objects; import java.net.URL; @@ -28,10 +29,7 @@ * Options for configuring the Durable Task Scheduler worker. */ public class DurableTaskSchedulerWorkerOptions { - @NotBlank(message = "Endpoint address is required") private String endpointAddress = ""; - - @NotBlank(message = "Task hub name is required") private String taskHubName = ""; private TokenCredential credential; @@ -53,7 +51,7 @@ public DurableTaskSchedulerWorkerOptions() { * @return A new DurableTaskSchedulerWorkerOptions object. */ public static DurableTaskSchedulerWorkerOptions fromConnectionString(String connectionString, @Nullable TokenCredential credential) { - DurableTaskSchedulerConnectionString parsedConnectionString = DurableTaskSchedulerConnectionString.parse(connectionString); + DurableTaskSchedulerConnectionString parsedConnectionString = new DurableTaskSchedulerConnectionString(connectionString); return fromConnectionString(parsedConnectionString, credential); } @@ -207,9 +205,8 @@ public void validate() { * Creates a gRPC channel using the configured options. * * @return A configured gRPC channel. - * @throws MalformedURLException If the endpoint address is invalid. */ - public Channel createGrpcChannel() throws MalformedURLException { + public Channel createGrpcChannel() { // Create token cache only if credential is not null AccessTokenCache tokenCache = null; if (credential != null) { @@ -225,13 +222,19 @@ public Channel createGrpcChannel() throws MalformedURLException { endpoint = "https://" + endpoint; } - URL url = new URL(endpoint); + URL url; + try { + url = new URL(endpoint); + } catch (MalformedURLException e) { + throw new IllegalArgumentException("Invalid endpoint URL: " + endpoint); + } String authority = url.getHost(); if (url.getPort() != -1) { authority += ":" + url.getPort(); } // Create metadata interceptor to add task hub name and auth token + AccessTokenCache finalTokenCache = tokenCache; ClientInterceptor metadataInterceptor = new ClientInterceptor() { @Override public ClientCall interceptCall( @@ -248,8 +251,8 @@ public void start(ClientCall.Listener responseListener, Metadata headers) ); // Add authorization token if credentials are configured - if (tokenCache != null) { - String token = tokenCache.getToken().getToken(); + if (finalTokenCache != null) { + String token = finalTokenCache.getToken().getToken(); headers.put( Metadata.Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER), "Bearer " + token diff --git a/client/build.gradle b/client/build.gradle index 277ec5ec..961f0223 100644 --- a/client/build.gradle +++ b/client/build.gradle @@ -30,12 +30,6 @@ dependencies { compileOnly "org.apache.tomcat:annotations-api:6.0.53" - // https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core - implementation "com.fasterxml.jackson.core:jackson-core:${jacksonVersion}" - implementation "com.fasterxml.jackson.core:jackson-databind:${jacksonVersion}" - implementation "com.fasterxml.jackson.core:jackson-annotations:${jacksonVersion}" - implementation "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:${jacksonVersion}" - // Azure dependencies for authentication implementation "com.azure:azure-core:${azureCoreVersion}" implementation "com.azure:azure-identity:${azureIdentityVersion}" diff --git a/client/src/main/java/com/microsoft/durabletask/DurableTaskGrpcClient.java b/client/src/main/java/com/microsoft/durabletask/DurableTaskGrpcClient.java index 95bb984a..11152ef6 100644 --- a/client/src/main/java/com/microsoft/durabletask/DurableTaskGrpcClient.java +++ b/client/src/main/java/com/microsoft/durabletask/DurableTaskGrpcClient.java @@ -21,7 +21,7 @@ /** * Durable Task client implementation that uses gRPC to connect to a remote "sidecar" process. */ -final class DurableTaskGrpcClient extends DurableTaskClient { +public final class DurableTaskGrpcClient extends DurableTaskClient { private static final int DEFAULT_PORT = 4001; private static final Logger logger = Logger.getLogger(DurableTaskGrpcClient.class.getPackage().getName()); diff --git a/samples/build.gradle b/samples/build.gradle index 7e41d166..af98f1e6 100644 --- a/samples/build.gradle +++ b/samples/build.gradle @@ -14,6 +14,7 @@ application { } dependencies { + implementation project(':client') implementation project(':azuremanaged:client') implementation project(':azuremanaged:worker') diff --git a/samples/src/main/java/io/durabletask/samples/WebAppToDurableTaskSchedulerSample.java b/samples/src/main/java/io/durabletask/samples/WebAppToDurableTaskSchedulerSample.java index f11a410a..8b4b7581 100644 --- a/samples/src/main/java/io/durabletask/samples/WebAppToDurableTaskSchedulerSample.java +++ b/samples/src/main/java/io/durabletask/samples/WebAppToDurableTaskSchedulerSample.java @@ -4,11 +4,9 @@ import com.azure.core.credential.TokenCredential; import com.microsoft.durabletask.*; + import com.microsoft.durabletask.client.azuremanaged.DurableTaskSchedulerClientExtensions; -import com.microsoft.durabletask.client.azuremanaged.DurableTaskSchedulerClientOptions; import com.microsoft.durabletask.worker.azuremanaged.DurableTaskSchedulerWorkerExtensions; -import com.microsoft.durabletask.worker.azuremanaged.DurableTaskSchedulerWorkerOptions; - import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.*; @@ -125,10 +123,10 @@ public DurableTaskClient durableTaskClient( TokenCredential tokenCredential) { // Create client using Azure-managed extensions - return DurableTaskSchedulerClientExtensions.createClient( + return DurableTaskSchedulerClientExtensions.createClientBuilder( properties.getEndpoint(), properties.getTaskHubName(), - tokenCredential); + tokenCredential).build(); } } From 02f92ea172c91dba6a94ec2061e4fe6fd3f58e60 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 19 Mar 2025 13:31:25 -0700 Subject: [PATCH 34/53] save --- azuremanaged/client/build.gradle | 2 - azuremanaged/worker/build.gradle | 2 - .../WebAppToDurableTaskSchedulerSample.java | 117 +++++++++++------- 3 files changed, 74 insertions(+), 47 deletions(-) diff --git a/azuremanaged/client/build.gradle b/azuremanaged/client/build.gradle index 33141032..e0ae507b 100644 --- a/azuremanaged/client/build.gradle +++ b/azuremanaged/client/build.gradle @@ -19,8 +19,6 @@ version = '1.5.0' archivesBaseName = 'durabletask-azuremanaged-client' def grpcVersion = '1.59.0' -def protocVersion = '3.12.0' -def jacksonVersion = '2.15.3' def azureCoreVersion = '1.45.0' def azureIdentityVersion = '1.11.1' def durabletaskClientVersion = '1.5.0' diff --git a/azuremanaged/worker/build.gradle b/azuremanaged/worker/build.gradle index 5e852a78..fdd7c9d9 100644 --- a/azuremanaged/worker/build.gradle +++ b/azuremanaged/worker/build.gradle @@ -19,8 +19,6 @@ version = '1.5.0' archivesBaseName = 'durabletask-azuremanaged-worker' def grpcVersion = '1.59.0' -def protocVersion = '3.12.0' -def jacksonVersion = '2.15.3' def azureCoreVersion = '1.45.0' def azureIdentityVersion = '1.11.1' def durabletaskClientVersion = '1.5.0' diff --git a/samples/src/main/java/io/durabletask/samples/WebAppToDurableTaskSchedulerSample.java b/samples/src/main/java/io/durabletask/samples/WebAppToDurableTaskSchedulerSample.java index 8b4b7581..e169ada1 100644 --- a/samples/src/main/java/io/durabletask/samples/WebAppToDurableTaskSchedulerSample.java +++ b/samples/src/main/java/io/durabletask/samples/WebAppToDurableTaskSchedulerSample.java @@ -4,9 +4,8 @@ import com.azure.core.credential.TokenCredential; import com.microsoft.durabletask.*; - -import com.microsoft.durabletask.client.azuremanaged.DurableTaskSchedulerClientExtensions; -import com.microsoft.durabletask.worker.azuremanaged.DurableTaskSchedulerWorkerExtensions; +import com.microsoft.durabletask.azuremanaged.client.DurableTaskSchedulerClientExtensions; +import com.microsoft.durabletask.azuremanaged.worker.DurableTaskSchedulerWorkerExtensions; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.*; @@ -65,53 +64,85 @@ public DurableTaskGrpcWorker durableTaskWorker( properties.getTaskHubName(), tokenCredential); - // Add orchestrations - workerBuilder.addOrchestration("ProcessOrderOrchestration", ctx -> { - // Get the order input as JSON string - String orderJson = ctx.getInput(String.class); - - // Process the order through multiple activities - boolean isValid = ctx.callActivity("ValidateOrder", orderJson, Boolean.class).await(); - if (!isValid) { - ctx.complete("{\"status\": \"FAILED\", \"message\": \"Order validation failed\"}"); - return; - } - - // Process payment - String paymentResult = ctx.callActivity("ProcessPayment", orderJson, String.class).await(); - if (!paymentResult.contains("\"success\":true")) { - ctx.complete("{\"status\": \"FAILED\", \"message\": \"Payment processing failed\"}"); - return; + // Add orchestrations using the factory pattern + workerBuilder.addOrchestration(new TaskOrchestrationFactory() { + @Override + public String getName() { return "ProcessOrderOrchestration"; } + + @Override + public TaskOrchestration create() { + return ctx -> { + // Get the order input as JSON string + String orderJson = ctx.getInput(String.class); + + // Process the order through multiple activities + boolean isValid = ctx.callActivity("ValidateOrder", orderJson, Boolean.class).await(); + if (!isValid) { + ctx.complete("{\"status\": \"FAILED\", \"message\": \"Order validation failed\"}"); + return; + } + + // Process payment + String paymentResult = ctx.callActivity("ProcessPayment", orderJson, String.class).await(); + if (!paymentResult.contains("\"success\":true")) { + ctx.complete("{\"status\": \"FAILED\", \"message\": \"Payment processing failed\"}"); + return; + } + + // Ship order + String shipmentResult = ctx.callActivity("ShipOrder", orderJson, String.class).await(); + + // Return the final result + ctx.complete("{\"status\": \"SUCCESS\", " + + "\"payment\": " + paymentResult + ", " + + "\"shipment\": " + shipmentResult + "}"); + }; } - - // Ship order - String shipmentResult = ctx.callActivity("ShipOrder", orderJson, String.class).await(); - - // Return the final result - ctx.complete("{\"status\": \"SUCCESS\", " + - "\"payment\": " + paymentResult + ", " + - "\"shipment\": " + shipmentResult + "}"); }); - // Add activity implementations - workerBuilder.addActivity("ValidateOrder", ctx -> { - String orderJson = ctx.getInput(String.class); - // Simple validation - check if order contains amount and it's greater than 0 - return orderJson.contains("\"amount\"") && !orderJson.contains("\"amount\":0"); + // Add activities using the factory pattern + workerBuilder.addActivity(new TaskActivityFactory() { + @Override + public String getName() { return "ValidateOrder"; } + + @Override + public TaskActivity create() { + return ctx -> { + String orderJson = ctx.getInput(String.class); + // Simple validation - check if order contains amount and it's greater than 0 + return orderJson.contains("\"amount\"") && !orderJson.contains("\"amount\":0"); + }; + } }); - workerBuilder.addActivity("ProcessPayment", ctx -> { - String orderJson = ctx.getInput(String.class); - // Simulate payment processing - sleep(1000); // Simulate processing time - return "{\"success\":true, \"transactionId\":\"TXN" + System.currentTimeMillis() + "\"}"; + workerBuilder.addActivity(new TaskActivityFactory() { + @Override + public String getName() { return "ProcessPayment"; } + + @Override + public TaskActivity create() { + return ctx -> { + String orderJson = ctx.getInput(String.class); + // Simulate payment processing + sleep(1000); // Simulate processing time + return "{\"success\":true, \"transactionId\":\"TXN" + System.currentTimeMillis() + "\"}"; + }; + } }); - workerBuilder.addActivity("ShipOrder", ctx -> { - String orderJson = ctx.getInput(String.class); - // Simulate shipping process - sleep(1000); // Simulate processing time - return "{\"trackingNumber\":\"TRACK" + System.currentTimeMillis() + "\"}"; + workerBuilder.addActivity(new TaskActivityFactory() { + @Override + public String getName() { return "ShipOrder"; } + + @Override + public TaskActivity create() { + return ctx -> { + String orderJson = ctx.getInput(String.class); + // Simulate shipping process + sleep(1000); // Simulate processing time + return "{\"trackingNumber\":\"TRACK" + System.currentTimeMillis() + "\"}"; + }; + } }); return workerBuilder.build(); From 2b9d8eb64edf1395efca362b7ca8ab7a513cf0b5 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 19 Mar 2025 13:52:02 -0700 Subject: [PATCH 35/53] fix --- samples/build.gradle | 3 ++- .../WebAppToDurableTaskSchedulerSample.java | 20 +++++++++---------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/samples/build.gradle b/samples/build.gradle index af98f1e6..efff1293 100644 --- a/samples/build.gradle +++ b/samples/build.gradle @@ -7,6 +7,7 @@ plugins { group 'io.durabletask' version = '0.1.0' def grpcVersion = '1.59.0' +def durabletaskClientVersion = '1.5.0' archivesBaseName = 'durabletask-samples' application { @@ -14,10 +15,10 @@ application { } dependencies { - implementation project(':client') implementation project(':azuremanaged:client') implementation project(':azuremanaged:worker') + implementation "com.microsoft:durabletask-client:${durabletaskClientVersion}" implementation 'org.springframework.boot:spring-boot-starter-web' implementation platform("org.springframework.boot:spring-boot-dependencies:2.5.2") implementation 'org.springframework.boot:spring-boot-starter' diff --git a/samples/src/main/java/io/durabletask/samples/WebAppToDurableTaskSchedulerSample.java b/samples/src/main/java/io/durabletask/samples/WebAppToDurableTaskSchedulerSample.java index e169ada1..bd1e08e8 100644 --- a/samples/src/main/java/io/durabletask/samples/WebAppToDurableTaskSchedulerSample.java +++ b/samples/src/main/java/io/durabletask/samples/WebAppToDurableTaskSchedulerSample.java @@ -4,8 +4,8 @@ import com.azure.core.credential.TokenCredential; import com.microsoft.durabletask.*; -import com.microsoft.durabletask.azuremanaged.client.DurableTaskSchedulerClientExtensions; -import com.microsoft.durabletask.azuremanaged.worker.DurableTaskSchedulerWorkerExtensions; +import com.microsoft.durabletask.client.azuremanaged.DurableTaskSchedulerClientExtensions; +import com.microsoft.durabletask.worker.azuremanaged.DurableTaskSchedulerWorkerExtensions; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.*; @@ -39,7 +39,7 @@ public class WebAppToDurableTaskSchedulerSample { public static void main(String[] args) { ConfigurableApplicationContext context = SpringApplication.run(WebAppToDurableTaskSchedulerSample.class, args); - + // Get the worker bean and start it DurableTaskGrpcWorker worker = context.getBean(DurableTaskGrpcWorker.class); worker.start(); @@ -57,7 +57,7 @@ public TokenCredential tokenCredential() { public DurableTaskGrpcWorker durableTaskWorker( DurableTaskProperties properties, TokenCredential tokenCredential) { - + // Create worker using Azure-managed extensions DurableTaskGrpcWorkerBuilder workerBuilder = DurableTaskSchedulerWorkerExtensions.createWorkerBuilder( properties.getEndpoint(), @@ -91,7 +91,7 @@ public TaskOrchestration create() { // Ship order String shipmentResult = ctx.callActivity("ShipOrder", orderJson, String.class).await(); - + // Return the final result ctx.complete("{\"status\": \"SUCCESS\", " + "\"payment\": " + paymentResult + ", " + @@ -152,7 +152,7 @@ public TaskActivity create() { public DurableTaskClient durableTaskClient( DurableTaskProperties properties, TokenCredential tokenCredential) { - + // Create client using Azure-managed extensions return DurableTaskSchedulerClientExtensions.createClientBuilder( properties.getEndpoint(), @@ -186,14 +186,14 @@ public OrderController(DurableTaskClient client) { @PostMapping public String createOrder(@RequestBody String orderJson) throws Exception { String instanceId = client.scheduleNewOrchestrationInstance( - "ProcessOrderOrchestration", + "ProcessOrderOrchestration", orderJson ); // Wait for the orchestration to complete with a timeout OrchestrationMetadata metadata = client.waitForInstanceCompletion( - instanceId, - Duration.ofSeconds(30), + instanceId, + Duration.ofSeconds(30), true ); @@ -212,4 +212,4 @@ public String getOrder(@PathVariable String instanceId) throws Exception { } return metadata.readOutputAs(String.class); } -} \ No newline at end of file +} \ No newline at end of file From 3eca075e9dbcaa2048f30a07b944834b2d2a1be6 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 19 Mar 2025 14:22:20 -0700 Subject: [PATCH 36/53] cleanup --- client/build.gradle | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/client/build.gradle b/client/build.gradle index 961f0223..07be5a18 100644 --- a/client/build.gradle +++ b/client/build.gradle @@ -14,8 +14,6 @@ archivesBaseName = 'durabletask-client' def grpcVersion = '1.59.0' def protocVersion = '3.12.0' def jacksonVersion = '2.15.3' -def azureCoreVersion = '1.45.0' -def azureIdentityVersion = '1.11.1' // When build on local, you need to set this value to your local jdk11 directory. // Java11 is used to compile and run all the tests. // Example for Windows: C:/Program Files/Java/openjdk-11.0.12_7/ @@ -30,10 +28,12 @@ dependencies { compileOnly "org.apache.tomcat:annotations-api:6.0.53" - // Azure dependencies for authentication - implementation "com.azure:azure-core:${azureCoreVersion}" - implementation "com.azure:azure-identity:${azureIdentityVersion}" - + // https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core + implementation "com.fasterxml.jackson.core:jackson-core:${jacksonVersion}" + implementation "com.fasterxml.jackson.core:jackson-databind:${jacksonVersion}" + implementation "com.fasterxml.jackson.core:jackson-annotations:${jacksonVersion}" + implementation "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:${jacksonVersion}" + testImplementation(platform('org.junit:junit-bom:5.7.2')) testImplementation('org.junit.jupiter:junit-jupiter') } From 6e9a6e8a24ff6058ea8e7eff2cf87dc29a1e36d4 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 19 Mar 2025 14:40:54 -0700 Subject: [PATCH 37/53] cleanup --- azuremanaged/client/build.gradle | 2 -- azuremanaged/shared/build.gradle | 2 -- azuremanaged/worker/build.gradle | 2 -- 3 files changed, 6 deletions(-) diff --git a/azuremanaged/client/build.gradle b/azuremanaged/client/build.gradle index e0ae507b..38435a67 100644 --- a/azuremanaged/client/build.gradle +++ b/azuremanaged/client/build.gradle @@ -14,8 +14,6 @@ plugins { id 'signing' } -group 'com.microsoft' -version = '1.5.0' archivesBaseName = 'durabletask-azuremanaged-client' def grpcVersion = '1.59.0' diff --git a/azuremanaged/shared/build.gradle b/azuremanaged/shared/build.gradle index 0fb13392..52a64a10 100644 --- a/azuremanaged/shared/build.gradle +++ b/azuremanaged/shared/build.gradle @@ -4,8 +4,6 @@ plugins { id 'idea' } -group 'com.microsoft' -version = '1.5.0' archivesBaseName = 'durabletask-azuremanaged-shared' def grpcVersion = '1.59.0' diff --git a/azuremanaged/worker/build.gradle b/azuremanaged/worker/build.gradle index fdd7c9d9..16368864 100644 --- a/azuremanaged/worker/build.gradle +++ b/azuremanaged/worker/build.gradle @@ -14,8 +14,6 @@ plugins { id 'signing' } -group 'com.microsoft' -version = '1.5.0' archivesBaseName = 'durabletask-azuremanaged-worker' def grpcVersion = '1.59.0' From d9e20ec9ded1ff8e8d939cf90598a4afc38a1c6f Mon Sep 17 00:00:00 2001 From: wangbill <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 20 Mar 2025 15:49:42 -0700 Subject: [PATCH 38/53] unit tests --- azuremanaged/client/build.gradle | 8 +- ...ableTaskSchedulerClientExtensionsTest.java | 153 ++++++++++++ ...DurableTaskSchedulerClientOptionsTest.java | 231 ++++++++++++++++++ azuremanaged/shared/build.gradle | 7 + .../DurableTaskSchedulerConnectionString.java | 7 +- .../azuremanaged/AccessTokenCacheTest.java | 116 +++++++++ ...ableTaskSchedulerConnectionStringTest.java | 172 +++++++++++++ azuremanaged/worker/build.gradle | 8 +- ...ableTaskSchedulerWorkerExtensionsTest.java | 154 ++++++++++++ ...DurableTaskSchedulerWorkerOptionsTest.java | 231 ++++++++++++++++++ 10 files changed, 1082 insertions(+), 5 deletions(-) create mode 100644 azuremanaged/client/src/test/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientExtensionsTest.java create mode 100644 azuremanaged/client/src/test/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientOptionsTest.java create mode 100644 azuremanaged/shared/src/test/java/com/microsoft/durabletask/shared/azuremanaged/AccessTokenCacheTest.java create mode 100644 azuremanaged/shared/src/test/java/com/microsoft/durabletask/shared/azuremanaged/DurableTaskSchedulerConnectionStringTest.java create mode 100644 azuremanaged/worker/src/test/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerExtensionsTest.java create mode 100644 azuremanaged/worker/src/test/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerOptionsTest.java diff --git a/azuremanaged/client/build.gradle b/azuremanaged/client/build.gradle index 38435a67..cf760fef 100644 --- a/azuremanaged/client/build.gradle +++ b/azuremanaged/client/build.gradle @@ -38,8 +38,12 @@ dependencies { implementation "com.azure:azure-core:${azureCoreVersion}" implementation "com.azure:azure-identity:${azureIdentityVersion}" - testImplementation(platform('org.junit:junit-bom:5.7.2')) - testImplementation('org.junit.jupiter:junit-jupiter') + // Test dependencies + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.0' + testImplementation 'org.junit.jupiter:junit-jupiter-params:5.10.0' + testImplementation 'org.mockito:mockito-core:5.3.1' + testImplementation 'org.mockito:mockito-junit-jupiter:5.3.1' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.10.0' } compileJava { diff --git a/azuremanaged/client/src/test/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientExtensionsTest.java b/azuremanaged/client/src/test/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientExtensionsTest.java new file mode 100644 index 00000000..09aef814 --- /dev/null +++ b/azuremanaged/client/src/test/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientExtensionsTest.java @@ -0,0 +1,153 @@ +package com.microsoft.durabletask.client.azuremanaged; + +import com.azure.core.credential.TokenCredential; +import com.microsoft.durabletask.DurableTaskGrpcClientBuilder; +import io.grpc.Channel; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +/** + * Unit tests for {@link DurableTaskSchedulerClientExtensions}. + */ +@ExtendWith(MockitoExtension.class) +public class DurableTaskSchedulerClientExtensionsTest { + + private static final String VALID_CONNECTION_STRING = + "Endpoint=https://example.com;Authentication=ManagedIdentity;TaskHub=myTaskHub"; + private static final String VALID_ENDPOINT = "https://example.com"; + private static final String VALID_TASKHUB = "myTaskHub"; + + @Mock + private DurableTaskGrpcClientBuilder mockBuilder; + + @Mock + private TokenCredential mockCredential; + + @Test + @DisplayName("useDurableTaskScheduler with connection string should configure builder correctly") + public void useDurableTaskScheduler_WithConnectionString_ConfiguresBuilder() { + // Arrange + when(mockBuilder.grpcChannel(any(Channel.class))).thenReturn(mockBuilder); + + // Act + DurableTaskSchedulerClientExtensions.useDurableTaskScheduler( + mockBuilder, VALID_CONNECTION_STRING, mockCredential); + + // Assert + verify(mockBuilder).grpcChannel(any(Channel.class)); + } + + @Test + @DisplayName("useDurableTaskScheduler with connection string should throw for null builder") + public void useDurableTaskScheduler_WithConnectionString_ThrowsForNullBuilder() { + // Act & Assert + assertThrows(NullPointerException.class, + () -> DurableTaskSchedulerClientExtensions.useDurableTaskScheduler( + null, VALID_CONNECTION_STRING, mockCredential)); + } + + @Test + @DisplayName("useDurableTaskScheduler with connection string should throw for null connection string") + public void useDurableTaskScheduler_WithConnectionString_ThrowsForNullConnectionString() { + // Act & Assert + assertThrows(NullPointerException.class, + () -> DurableTaskSchedulerClientExtensions.useDurableTaskScheduler( + mockBuilder, null, mockCredential)); + } + + @Test + @DisplayName("useDurableTaskScheduler with explicit parameters should configure builder correctly") + public void useDurableTaskScheduler_WithExplicitParameters_ConfiguresBuilder() { + // Arrange + when(mockBuilder.grpcChannel(any(Channel.class))).thenReturn(mockBuilder); + + // Act + DurableTaskSchedulerClientExtensions.useDurableTaskScheduler( + mockBuilder, VALID_ENDPOINT, VALID_TASKHUB, mockCredential); + + // Assert + verify(mockBuilder).grpcChannel(any(Channel.class)); + } + + @Test + @DisplayName("useDurableTaskScheduler with explicit parameters should throw for null builder") + public void useDurableTaskScheduler_WithExplicitParameters_ThrowsForNullBuilder() { + // Act & Assert + assertThrows(NullPointerException.class, + () -> DurableTaskSchedulerClientExtensions.useDurableTaskScheduler( + null, VALID_ENDPOINT, VALID_TASKHUB, mockCredential)); + } + + @Test + @DisplayName("useDurableTaskScheduler with explicit parameters should throw for null endpoint") + public void useDurableTaskScheduler_WithExplicitParameters_ThrowsForNullEndpoint() { + // Act & Assert + assertThrows(NullPointerException.class, + () -> DurableTaskSchedulerClientExtensions.useDurableTaskScheduler( + mockBuilder, null, VALID_TASKHUB, mockCredential)); + } + + @Test + @DisplayName("useDurableTaskScheduler with explicit parameters should throw for null task hub name") + public void useDurableTaskScheduler_WithExplicitParameters_ThrowsForNullTaskHubName() { + // Act & Assert + assertThrows(NullPointerException.class, + () -> DurableTaskSchedulerClientExtensions.useDurableTaskScheduler( + mockBuilder, VALID_ENDPOINT, null, mockCredential)); + } + + @Test + @DisplayName("createClientBuilder with connection string should create valid builder") + public void createClientBuilder_WithConnectionString_CreatesValidBuilder() { + // Act + DurableTaskGrpcClientBuilder result = + DurableTaskSchedulerClientExtensions.createClientBuilder(VALID_CONNECTION_STRING, mockCredential); + + // Assert + assertNotNull(result); + } + + @Test + @DisplayName("createClientBuilder with connection string should throw for null connection string") + public void createClientBuilder_WithConnectionString_ThrowsForNullConnectionString() { + // Act & Assert + assertThrows(NullPointerException.class, + () -> DurableTaskSchedulerClientExtensions.createClientBuilder(null, mockCredential)); + } + + @Test + @DisplayName("createClientBuilder with explicit parameters should create valid builder") + public void createClientBuilder_WithExplicitParameters_CreatesValidBuilder() { + // Act + DurableTaskGrpcClientBuilder result = + DurableTaskSchedulerClientExtensions.createClientBuilder( + VALID_ENDPOINT, VALID_TASKHUB, mockCredential); + + // Assert + assertNotNull(result); + } + + @Test + @DisplayName("createClientBuilder with explicit parameters should throw for null endpoint") + public void createClientBuilder_WithExplicitParameters_ThrowsForNullEndpoint() { + // Act & Assert + assertThrows(NullPointerException.class, + () -> DurableTaskSchedulerClientExtensions.createClientBuilder( + null, VALID_TASKHUB, mockCredential)); + } + + @Test + @DisplayName("createClientBuilder with explicit parameters should throw for null task hub name") + public void createClientBuilder_WithExplicitParameters_ThrowsForNullTaskHubName() { + // Act & Assert + assertThrows(NullPointerException.class, + () -> DurableTaskSchedulerClientExtensions.createClientBuilder( + VALID_ENDPOINT, null, mockCredential)); + } +} \ No newline at end of file diff --git a/azuremanaged/client/src/test/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientOptionsTest.java b/azuremanaged/client/src/test/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientOptionsTest.java new file mode 100644 index 00000000..3c571e27 --- /dev/null +++ b/azuremanaged/client/src/test/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientOptionsTest.java @@ -0,0 +1,231 @@ +package com.microsoft.durabletask.client.azuremanaged; + +import com.azure.core.credential.TokenCredential; +import com.microsoft.durabletask.shared.azuremanaged.DurableTaskSchedulerConnectionString; +import io.grpc.Channel; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.Duration; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for {@link DurableTaskSchedulerClientOptions}. + */ +@ExtendWith(MockitoExtension.class) +public class DurableTaskSchedulerClientOptionsTest { + + private static final String VALID_ENDPOINT = "https://example.com"; + private static final String VALID_TASKHUB = "myTaskHub"; + private static final String VALID_CONNECTION_STRING = + "Endpoint=https://example.com;Authentication=ManagedIdentity;TaskHub=myTaskHub"; + + @Mock + private TokenCredential mockCredential; + + @Test + @DisplayName("Default constructor should set default values") + public void defaultConstructor_SetsDefaultValues() { + // Act + DurableTaskSchedulerClientOptions options = new DurableTaskSchedulerClientOptions(); + + // Assert + assertEquals("", options.getEndpointAddress()); + assertEquals("", options.getTaskHubName()); + assertNull(options.getCredential()); + assertEquals("https://durabletask.io", options.getResourceId()); + assertFalse(options.isAllowInsecureCredentials()); + assertEquals(Duration.ofMinutes(5), options.getTokenRefreshMargin()); + } + + @Test + @DisplayName("fromConnectionString should parse connection string correctly") + public void fromConnectionString_ParsesConnectionStringCorrectly() { + // Act + DurableTaskSchedulerClientOptions options = + DurableTaskSchedulerClientOptions.fromConnectionString(VALID_CONNECTION_STRING, mockCredential); + + // Assert + assertEquals(VALID_ENDPOINT, options.getEndpointAddress()); + assertEquals(VALID_TASKHUB, options.getTaskHubName()); + assertSame(mockCredential, options.getCredential()); + assertFalse(options.isAllowInsecureCredentials()); + } + + @Test + @DisplayName("fromConnectionString should allow insecure credentials when credential is null") + public void fromConnectionString_AllowsInsecureCredentialsWhenCredentialIsNull() { + // Act + DurableTaskSchedulerClientOptions options = + DurableTaskSchedulerClientOptions.fromConnectionString(VALID_CONNECTION_STRING, null); + + // Assert + assertTrue(options.isAllowInsecureCredentials()); + assertNull(options.getCredential()); + } + + @Test + @DisplayName("setEndpointAddress should update endpoint address") + public void setEndpointAddress_UpdatesEndpointAddress() { + // Arrange + DurableTaskSchedulerClientOptions options = new DurableTaskSchedulerClientOptions(); + + // Act + DurableTaskSchedulerClientOptions result = options.setEndpointAddress(VALID_ENDPOINT); + + // Assert + assertEquals(VALID_ENDPOINT, options.getEndpointAddress()); + assertSame(options, result); // Builder pattern returns this + } + + @Test + @DisplayName("setTaskHubName should update task hub name") + public void setTaskHubName_UpdatesTaskHubName() { + // Arrange + DurableTaskSchedulerClientOptions options = new DurableTaskSchedulerClientOptions(); + + // Act + DurableTaskSchedulerClientOptions result = options.setTaskHubName(VALID_TASKHUB); + + // Assert + assertEquals(VALID_TASKHUB, options.getTaskHubName()); + assertSame(options, result); // Builder pattern returns this + } + + @Test + @DisplayName("setCredential should update credential") + public void setCredential_UpdatesCredential() { + // Arrange + DurableTaskSchedulerClientOptions options = new DurableTaskSchedulerClientOptions(); + + // Act + DurableTaskSchedulerClientOptions result = options.setCredential(mockCredential); + + // Assert + assertSame(mockCredential, options.getCredential()); + assertSame(options, result); // Builder pattern returns this + } + + @Test + @DisplayName("setResourceId should update resource ID") + public void setResourceId_UpdatesResourceId() { + // Arrange + DurableTaskSchedulerClientOptions options = new DurableTaskSchedulerClientOptions(); + String customResourceId = "https://custom-resource.example.com"; + + // Act + DurableTaskSchedulerClientOptions result = options.setResourceId(customResourceId); + + // Assert + assertEquals(customResourceId, options.getResourceId()); + assertSame(options, result); // Builder pattern returns this + } + + @Test + @DisplayName("setAllowInsecureCredentials should update allowInsecureCredentials") + public void setAllowInsecureCredentials_UpdatesAllowInsecureCredentials() { + // Arrange + DurableTaskSchedulerClientOptions options = new DurableTaskSchedulerClientOptions(); + + // Act + DurableTaskSchedulerClientOptions result = options.setAllowInsecureCredentials(true); + + // Assert + assertTrue(options.isAllowInsecureCredentials()); + assertSame(options, result); // Builder pattern returns this + } + + @Test + @DisplayName("setTokenRefreshMargin should update token refresh margin") + public void setTokenRefreshMargin_UpdatesTokenRefreshMargin() { + // Arrange + DurableTaskSchedulerClientOptions options = new DurableTaskSchedulerClientOptions(); + Duration customMargin = Duration.ofMinutes(10); + + // Act + DurableTaskSchedulerClientOptions result = options.setTokenRefreshMargin(customMargin); + + // Assert + assertEquals(customMargin, options.getTokenRefreshMargin()); + assertSame(options, result); // Builder pattern returns this + } + + @Test + @DisplayName("validate should not throw when options are valid") + public void validate_DoesNotThrowWhenOptionsAreValid() { + // Arrange + DurableTaskSchedulerClientOptions options = new DurableTaskSchedulerClientOptions() + .setEndpointAddress(VALID_ENDPOINT) + .setTaskHubName(VALID_TASKHUB); + + // Act & Assert + assertDoesNotThrow(() -> options.validate()); + } + + @Test + @DisplayName("validate should throw when endpoint address is null") + public void validate_ThrowsWhenEndpointAddressIsNull() { + // Arrange + DurableTaskSchedulerClientOptions options = new DurableTaskSchedulerClientOptions() + .setEndpointAddress(null) + .setTaskHubName(VALID_TASKHUB); + + // Act & Assert + assertThrows(NullPointerException.class, () -> options.validate()); + } + + @Test + @DisplayName("validate should throw when task hub name is null") + public void validate_ThrowsWhenTaskHubNameIsNull() { + // Arrange + DurableTaskSchedulerClientOptions options = new DurableTaskSchedulerClientOptions() + .setEndpointAddress(VALID_ENDPOINT) + .setTaskHubName(null); + + // Act & Assert + assertThrows(NullPointerException.class, () -> options.validate()); + } + + @Test + @DisplayName("createGrpcChannel should create a channel") + public void createGrpcChannel_CreatesChannel() { + // Arrange + DurableTaskSchedulerClientOptions options = new DurableTaskSchedulerClientOptions() + .setEndpointAddress(VALID_ENDPOINT) + .setTaskHubName(VALID_TASKHUB); + + // Act + Channel channel = options.createGrpcChannel(); + + // Assert + assertNotNull(channel); + } + + @Test + @DisplayName("createGrpcChannel should handle endpoints without protocol") + public void createGrpcChannel_HandlesEndpointsWithoutProtocol() { + // Arrange + DurableTaskSchedulerClientOptions options = new DurableTaskSchedulerClientOptions() + .setEndpointAddress("example.com") + .setTaskHubName(VALID_TASKHUB); + + // Act & Assert + assertDoesNotThrow(() -> options.createGrpcChannel()); + } + + @Test + @DisplayName("createGrpcChannel should throw for invalid URLs") + public void createGrpcChannel_ThrowsForInvalidUrls() { + // Arrange + DurableTaskSchedulerClientOptions options = new DurableTaskSchedulerClientOptions() + .setEndpointAddress("invalid:url") + .setTaskHubName(VALID_TASKHUB); + + // Act & Assert + assertThrows(IllegalArgumentException.class, () -> options.createGrpcChannel()); + } +} \ No newline at end of file diff --git a/azuremanaged/shared/build.gradle b/azuremanaged/shared/build.gradle index 52a64a10..1285ab1d 100644 --- a/azuremanaged/shared/build.gradle +++ b/azuremanaged/shared/build.gradle @@ -30,6 +30,13 @@ dependencies { testImplementation(platform('org.junit:junit-bom:5.7.2')) testImplementation('org.junit.jupiter:junit-jupiter') + + // Test dependencies + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.0' + testImplementation 'org.junit.jupiter:junit-jupiter-params:5.10.0' + testImplementation 'org.mockito:mockito-core:5.3.1' + testImplementation 'org.mockito:mockito-junit-jupiter:5.3.1' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.10.0' } compileJava { diff --git a/azuremanaged/shared/src/main/java/com/microsoft/durabletask/shared/azuremanaged/DurableTaskSchedulerConnectionString.java b/azuremanaged/shared/src/main/java/com/microsoft/durabletask/shared/azuremanaged/DurableTaskSchedulerConnectionString.java index 816850af..0e97ca61 100644 --- a/azuremanaged/shared/src/main/java/com/microsoft/durabletask/shared/azuremanaged/DurableTaskSchedulerConnectionString.java +++ b/azuremanaged/shared/src/main/java/com/microsoft/durabletask/shared/azuremanaged/DurableTaskSchedulerConnectionString.java @@ -21,10 +21,15 @@ public class DurableTaskSchedulerConnectionString { * @throws IllegalArgumentException If the connection string is invalid or missing required properties. */ public DurableTaskSchedulerConnectionString(String connectionString) { - if (connectionString == null || connectionString.isEmpty()) { + if (connectionString == null || connectionString.trim().isEmpty()) { throw new IllegalArgumentException("connectionString must not be null or empty"); } this.properties = parseConnectionString(connectionString); + + // Validate required properties + getRequiredValue("Endpoint"); + getRequiredValue("Authentication"); + getRequiredValue("TaskHub"); } /** diff --git a/azuremanaged/shared/src/test/java/com/microsoft/durabletask/shared/azuremanaged/AccessTokenCacheTest.java b/azuremanaged/shared/src/test/java/com/microsoft/durabletask/shared/azuremanaged/AccessTokenCacheTest.java new file mode 100644 index 00000000..a57deee8 --- /dev/null +++ b/azuremanaged/shared/src/test/java/com/microsoft/durabletask/shared/azuremanaged/AccessTokenCacheTest.java @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.durabletask.shared.azuremanaged; + +import com.azure.core.credential.AccessToken; +import com.azure.core.credential.TokenCredential; +import com.azure.core.credential.TokenRequestContext; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import reactor.core.publisher.Mono; + +import java.time.Duration; +import java.time.OffsetDateTime; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +/** + * Unit tests for {@link AccessTokenCache}. + */ +@ExtendWith(MockitoExtension.class) +public class AccessTokenCacheTest { + + @Mock + private TokenCredential mockCredential; + + private TokenRequestContext context; + private Duration margin; + private AccessTokenCache tokenCache; + + @BeforeEach + public void setup() { + context = new TokenRequestContext().addScopes("https://durabletask.io/.default"); + margin = Duration.ofMinutes(5); + tokenCache = new AccessTokenCache(mockCredential, context, margin); + } + + @Test + @DisplayName("getToken should fetch a new token when cache is empty") + public void getToken_WhenCacheEmpty_FetchesNewToken() { + // Arrange + AccessToken expectedToken = new AccessToken("token1", OffsetDateTime.now().plusHours(1)); + when(mockCredential.getToken(any(TokenRequestContext.class))).thenReturn(Mono.just(expectedToken)); + + // Act + AccessToken result = tokenCache.getToken(); + + // Assert + assertEquals(expectedToken, result); + verify(mockCredential, times(1)).getToken(context); + } + + @Test + @DisplayName("getToken should reuse cached token when not expired") + public void getToken_WhenTokenNotExpired_ReusesCachedToken() { + // Arrange + AccessToken expectedToken = new AccessToken("token1", OffsetDateTime.now().plusHours(1)); + when(mockCredential.getToken(any(TokenRequestContext.class))).thenReturn(Mono.just(expectedToken)); + + // Act + tokenCache.getToken(); // First call to cache the token + AccessToken result = tokenCache.getToken(); // Second call should use cached token + + // Assert + assertEquals(expectedToken, result); + verify(mockCredential, times(1)).getToken(context); // Should only be called once + } + + @Test + @DisplayName("getToken should fetch a new token when current token is expired") + public void getToken_WhenTokenExpired_FetchesNewToken() { + // Arrange + AccessToken expiredToken = new AccessToken("expired", OffsetDateTime.now().minusMinutes(1)); + AccessToken newToken = new AccessToken("new", OffsetDateTime.now().plusHours(1)); + + when(mockCredential.getToken(any(TokenRequestContext.class))) + .thenReturn(Mono.just(expiredToken)) + .thenReturn(Mono.just(newToken)); + + // Act + AccessToken firstResult = tokenCache.getToken(); + AccessToken secondResult = tokenCache.getToken(); + + // Assert + assertEquals(expiredToken, firstResult); + assertEquals(newToken, secondResult); + verify(mockCredential, times(2)).getToken(context); + } + + @Test + @DisplayName("getToken should fetch a new token when current token is about to expire within margin") + public void getToken_WhenTokenAboutToExpire_FetchesNewToken() { + // Arrange + AccessToken expiringToken = new AccessToken("expiring", OffsetDateTime.now().plus(margin.minusMinutes(1))); + AccessToken newToken = new AccessToken("new", OffsetDateTime.now().plusHours(1)); + + when(mockCredential.getToken(any(TokenRequestContext.class))) + .thenReturn(Mono.just(expiringToken)) + .thenReturn(Mono.just(newToken)); + + // Act + AccessToken firstResult = tokenCache.getToken(); + AccessToken secondResult = tokenCache.getToken(); + + // Assert + assertEquals(expiringToken, firstResult); + assertEquals(newToken, secondResult); + verify(mockCredential, times(2)).getToken(context); + } +} diff --git a/azuremanaged/shared/src/test/java/com/microsoft/durabletask/shared/azuremanaged/DurableTaskSchedulerConnectionStringTest.java b/azuremanaged/shared/src/test/java/com/microsoft/durabletask/shared/azuremanaged/DurableTaskSchedulerConnectionStringTest.java new file mode 100644 index 00000000..4047950e --- /dev/null +++ b/azuremanaged/shared/src/test/java/com/microsoft/durabletask/shared/azuremanaged/DurableTaskSchedulerConnectionStringTest.java @@ -0,0 +1,172 @@ +package com.microsoft.durabletask.shared.azuremanaged; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for {@link DurableTaskSchedulerConnectionString}. + */ +public class DurableTaskSchedulerConnectionStringTest { + + private static final String VALID_CONNECTION_STRING = + "Endpoint=https://example.com;Authentication=ManagedIdentity;TaskHub=myTaskHub"; + + @Test + @DisplayName("Constructor should parse valid connection string") + public void constructor_ParsesValidConnectionString() { + // Arrange & Act + DurableTaskSchedulerConnectionString connectionString = + new DurableTaskSchedulerConnectionString(VALID_CONNECTION_STRING); + + // Assert + assertEquals("https://example.com", connectionString.getEndpoint()); + assertEquals("ManagedIdentity", connectionString.getAuthentication()); + assertEquals("myTaskHub", connectionString.getTaskHubName()); + } + + @Test + @DisplayName("Constructor should handle connection string with whitespace") + public void constructor_HandlesWhitespace() { + // Arrange + String connectionStringWithSpaces = + "Endpoint = https://example.com ; Authentication = ManagedIdentity ; TaskHub = myTaskHub"; + + // Act + DurableTaskSchedulerConnectionString connectionString = + new DurableTaskSchedulerConnectionString(connectionStringWithSpaces); + + // Assert + assertEquals("https://example.com", connectionString.getEndpoint()); + assertEquals("ManagedIdentity", connectionString.getAuthentication()); + assertEquals("myTaskHub", connectionString.getTaskHubName()); + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {" "}) + @DisplayName("Constructor should throw for null or empty connection string") + public void constructor_ThrowsForNullOrEmptyConnectionString(String invalidInput) { + // Act & Assert + assertThrows(IllegalArgumentException.class, + () -> new DurableTaskSchedulerConnectionString(invalidInput)); + } + + @Test + @DisplayName("Constructor should throw when missing required Endpoint property") + public void constructor_ThrowsWhenMissingEndpoint() { + // Arrange + String missingEndpoint = "Authentication=ManagedIdentity;TaskHub=myTaskHub"; + + // Act & Assert + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> new DurableTaskSchedulerConnectionString(missingEndpoint)); + + assertTrue(exception.getMessage().contains("Endpoint")); + } + + @Test + @DisplayName("Constructor should throw when missing required Authentication property") + public void constructor_ThrowsWhenMissingAuthentication() { + // Arrange + String missingAuthentication = "Endpoint=https://example.com;TaskHub=myTaskHub"; + + // Act & Assert + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> new DurableTaskSchedulerConnectionString(missingAuthentication)); + + assertTrue(exception.getMessage().contains("Authentication")); + } + + @Test + @DisplayName("Constructor should throw when missing required TaskHub property") + public void constructor_ThrowsWhenMissingTaskHub() { + // Arrange + String missingTaskHub = "Endpoint=https://example.com;Authentication=ManagedIdentity"; + + // Act & Assert + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> new DurableTaskSchedulerConnectionString(missingTaskHub)); + + assertTrue(exception.getMessage().contains("TaskHub")); + } + + @Test + @DisplayName("getAdditionallyAllowedTenants should return split comma-separated values") + public void getAdditionallyAllowedTenants_ShouldSplitCommaValues() { + // Arrange + String connectionString = VALID_CONNECTION_STRING + + ";AdditionallyAllowedTenants=tenant1,tenant2,tenant3"; + + // Act + DurableTaskSchedulerConnectionString parsedString = + new DurableTaskSchedulerConnectionString(connectionString); + List tenants = parsedString.getAdditionallyAllowedTenants(); + + // Assert + assertNotNull(tenants); + assertEquals(3, tenants.size()); + assertEquals("tenant1", tenants.get(0)); + assertEquals("tenant2", tenants.get(1)); + assertEquals("tenant3", tenants.get(2)); + } + + @Test + @DisplayName("getAdditionallyAllowedTenants should return null when property not present") + public void getAdditionallyAllowedTenants_ReturnsNullWhenNotPresent() { + // Arrange & Act + DurableTaskSchedulerConnectionString connectionString = + new DurableTaskSchedulerConnectionString(VALID_CONNECTION_STRING); + + // Assert + assertNull(connectionString.getAdditionallyAllowedTenants()); + } + + @Test + @DisplayName("getClientId should return correct value when present") + public void getClientId_ReturnsValueWhenPresent() { + // Arrange + String connectionString = VALID_CONNECTION_STRING + ";ClientID=my-client-id"; + + // Act + DurableTaskSchedulerConnectionString parsedString = + new DurableTaskSchedulerConnectionString(connectionString); + + // Assert + assertEquals("my-client-id", parsedString.getClientId()); + } + + @Test + @DisplayName("getTenantId should return correct value when present") + public void getTenantId_ReturnsValueWhenPresent() { + // Arrange + String connectionString = VALID_CONNECTION_STRING + ";TenantId=my-tenant-id"; + + // Act + DurableTaskSchedulerConnectionString parsedString = + new DurableTaskSchedulerConnectionString(connectionString); + + // Assert + assertEquals("my-tenant-id", parsedString.getTenantId()); + } + + @Test + @DisplayName("getTokenFilePath should return correct value when present") + public void getTokenFilePath_ReturnsValueWhenPresent() { + // Arrange + String connectionString = VALID_CONNECTION_STRING + ";TokenFilePath=/path/to/token"; + + // Act + DurableTaskSchedulerConnectionString parsedString = + new DurableTaskSchedulerConnectionString(connectionString); + + // Assert + assertEquals("/path/to/token", parsedString.getTokenFilePath()); + } +} \ No newline at end of file diff --git a/azuremanaged/worker/build.gradle b/azuremanaged/worker/build.gradle index 16368864..2a8b2df4 100644 --- a/azuremanaged/worker/build.gradle +++ b/azuremanaged/worker/build.gradle @@ -37,8 +37,12 @@ dependencies { implementation "com.azure:azure-core:${azureCoreVersion}" implementation "com.azure:azure-identity:${azureIdentityVersion}" - testImplementation(platform('org.junit:junit-bom:5.7.2')) - testImplementation('org.junit.jupiter:junit-jupiter') + // Test dependencies + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.0' + testImplementation 'org.junit.jupiter:junit-jupiter-params:5.10.0' + testImplementation 'org.mockito:mockito-core:5.3.1' + testImplementation 'org.mockito:mockito-junit-jupiter:5.3.1' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.10.0' } compileJava { diff --git a/azuremanaged/worker/src/test/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerExtensionsTest.java b/azuremanaged/worker/src/test/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerExtensionsTest.java new file mode 100644 index 00000000..04e0e2c1 --- /dev/null +++ b/azuremanaged/worker/src/test/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerExtensionsTest.java @@ -0,0 +1,154 @@ +package com.microsoft.durabletask.worker.azuremanaged; + +import com.azure.core.credential.TokenCredential; +import com.microsoft.durabletask.DurableTaskGrpcWorkerBuilder; +import io.grpc.Channel; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.ArgumentCaptor; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +/** + * Unit tests for {@link DurableTaskSchedulerWorkerExtensions}. + */ +@ExtendWith(MockitoExtension.class) +public class DurableTaskSchedulerWorkerExtensionsTest { + + private static final String VALID_CONNECTION_STRING = + "Endpoint=https://example.com;Authentication=ManagedIdentity;TaskHub=myTaskHub"; + private static final String VALID_ENDPOINT = "https://example.com"; + private static final String VALID_TASKHUB = "myTaskHub"; + + @Mock + private DurableTaskGrpcWorkerBuilder mockBuilder; + + @Mock + private TokenCredential mockCredential; + + @Test + @DisplayName("useDurableTaskScheduler with connection string should configure builder correctly") + public void useDurableTaskScheduler_WithConnectionString_ConfiguresBuilder() { + // Arrange + when(mockBuilder.grpcChannel(any(Channel.class))).thenReturn(mockBuilder); + + // Act + DurableTaskSchedulerWorkerExtensions.useDurableTaskScheduler( + mockBuilder, VALID_CONNECTION_STRING, mockCredential); + + // Assert + verify(mockBuilder).grpcChannel(any(Channel.class)); + } + + @Test + @DisplayName("useDurableTaskScheduler with connection string should throw for null builder") + public void useDurableTaskScheduler_WithConnectionString_ThrowsForNullBuilder() { + // Act & Assert + assertThrows(NullPointerException.class, + () -> DurableTaskSchedulerWorkerExtensions.useDurableTaskScheduler( + null, VALID_CONNECTION_STRING, mockCredential)); + } + + @Test + @DisplayName("useDurableTaskScheduler with connection string should throw for null connection string") + public void useDurableTaskScheduler_WithConnectionString_ThrowsForNullConnectionString() { + // Act & Assert + assertThrows(NullPointerException.class, + () -> DurableTaskSchedulerWorkerExtensions.useDurableTaskScheduler( + mockBuilder, null, mockCredential)); + } + + @Test + @DisplayName("useDurableTaskScheduler with explicit parameters should configure builder correctly") + public void useDurableTaskScheduler_WithExplicitParameters_ConfiguresBuilder() { + // Arrange + when(mockBuilder.grpcChannel(any(Channel.class))).thenReturn(mockBuilder); + + // Act + DurableTaskSchedulerWorkerExtensions.useDurableTaskScheduler( + mockBuilder, VALID_ENDPOINT, VALID_TASKHUB, mockCredential); + + // Assert + verify(mockBuilder).grpcChannel(any(Channel.class)); + } + + @Test + @DisplayName("useDurableTaskScheduler with explicit parameters should throw for null builder") + public void useDurableTaskScheduler_WithExplicitParameters_ThrowsForNullBuilder() { + // Act & Assert + assertThrows(NullPointerException.class, + () -> DurableTaskSchedulerWorkerExtensions.useDurableTaskScheduler( + null, VALID_ENDPOINT, VALID_TASKHUB, mockCredential)); + } + + @Test + @DisplayName("useDurableTaskScheduler with explicit parameters should throw for null endpoint") + public void useDurableTaskScheduler_WithExplicitParameters_ThrowsForNullEndpoint() { + // Act & Assert + assertThrows(NullPointerException.class, + () -> DurableTaskSchedulerWorkerExtensions.useDurableTaskScheduler( + mockBuilder, null, VALID_TASKHUB, mockCredential)); + } + + @Test + @DisplayName("useDurableTaskScheduler with explicit parameters should throw for null task hub name") + public void useDurableTaskScheduler_WithExplicitParameters_ThrowsForNullTaskHubName() { + // Act & Assert + assertThrows(NullPointerException.class, + () -> DurableTaskSchedulerWorkerExtensions.useDurableTaskScheduler( + mockBuilder, VALID_ENDPOINT, null, mockCredential)); + } + + @Test + @DisplayName("createWorkerBuilder with connection string should create valid builder") + public void createWorkerBuilder_WithConnectionString_CreatesValidBuilder() { + // Act + DurableTaskGrpcWorkerBuilder result = + DurableTaskSchedulerWorkerExtensions.createWorkerBuilder(VALID_CONNECTION_STRING, mockCredential); + + // Assert + assertNotNull(result); + } + + @Test + @DisplayName("createWorkerBuilder with connection string should throw for null connection string") + public void createWorkerBuilder_WithConnectionString_ThrowsForNullConnectionString() { + // Act & Assert + assertThrows(NullPointerException.class, + () -> DurableTaskSchedulerWorkerExtensions.createWorkerBuilder(null, mockCredential)); + } + + @Test + @DisplayName("createWorkerBuilder with explicit parameters should create valid builder") + public void createWorkerBuilder_WithExplicitParameters_CreatesValidBuilder() { + // Act + DurableTaskGrpcWorkerBuilder result = + DurableTaskSchedulerWorkerExtensions.createWorkerBuilder( + VALID_ENDPOINT, VALID_TASKHUB, mockCredential); + + // Assert + assertNotNull(result); + } + + @Test + @DisplayName("createWorkerBuilder with explicit parameters should throw for null endpoint") + public void createWorkerBuilder_WithExplicitParameters_ThrowsForNullEndpoint() { + // Act & Assert + assertThrows(NullPointerException.class, + () -> DurableTaskSchedulerWorkerExtensions.createWorkerBuilder( + null, VALID_TASKHUB, mockCredential)); + } + + @Test + @DisplayName("createWorkerBuilder with explicit parameters should throw for null task hub name") + public void createWorkerBuilder_WithExplicitParameters_ThrowsForNullTaskHubName() { + // Act & Assert + assertThrows(NullPointerException.class, + () -> DurableTaskSchedulerWorkerExtensions.createWorkerBuilder( + VALID_ENDPOINT, null, mockCredential)); + } +} \ No newline at end of file diff --git a/azuremanaged/worker/src/test/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerOptionsTest.java b/azuremanaged/worker/src/test/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerOptionsTest.java new file mode 100644 index 00000000..cc422923 --- /dev/null +++ b/azuremanaged/worker/src/test/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerOptionsTest.java @@ -0,0 +1,231 @@ +package com.microsoft.durabletask.worker.azuremanaged; + +import com.azure.core.credential.TokenCredential; +import com.microsoft.durabletask.shared.azuremanaged.DurableTaskSchedulerConnectionString; +import io.grpc.Channel; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.Duration; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for {@link DurableTaskSchedulerWorkerOptions}. + */ +@ExtendWith(MockitoExtension.class) +public class DurableTaskSchedulerWorkerOptionsTest { + + private static final String VALID_ENDPOINT = "https://example.com"; + private static final String VALID_TASKHUB = "myTaskHub"; + private static final String VALID_CONNECTION_STRING = + "Endpoint=https://example.com;Authentication=ManagedIdentity;TaskHub=myTaskHub"; + + @Mock + private TokenCredential mockCredential; + + @Test + @DisplayName("Default constructor should set default values") + public void defaultConstructor_SetsDefaultValues() { + // Act + DurableTaskSchedulerWorkerOptions options = new DurableTaskSchedulerWorkerOptions(); + + // Assert + assertEquals("", options.getEndpointAddress()); + assertEquals("", options.getTaskHubName()); + assertNull(options.getCredential()); + assertEquals("https://durabletask.io", options.getResourceId()); + assertFalse(options.isAllowInsecureCredentials()); + assertEquals(Duration.ofMinutes(5), options.getTokenRefreshMargin()); + } + + @Test + @DisplayName("fromConnectionString should parse connection string correctly") + public void fromConnectionString_ParsesConnectionStringCorrectly() { + // Act + DurableTaskSchedulerWorkerOptions options = + DurableTaskSchedulerWorkerOptions.fromConnectionString(VALID_CONNECTION_STRING, mockCredential); + + // Assert + assertEquals(VALID_ENDPOINT, options.getEndpointAddress()); + assertEquals(VALID_TASKHUB, options.getTaskHubName()); + assertSame(mockCredential, options.getCredential()); + assertFalse(options.isAllowInsecureCredentials()); + } + + @Test + @DisplayName("fromConnectionString should allow insecure credentials when credential is null") + public void fromConnectionString_AllowsInsecureCredentialsWhenCredentialIsNull() { + // Act + DurableTaskSchedulerWorkerOptions options = + DurableTaskSchedulerWorkerOptions.fromConnectionString(VALID_CONNECTION_STRING, null); + + // Assert + assertTrue(options.isAllowInsecureCredentials()); + assertNull(options.getCredential()); + } + + @Test + @DisplayName("setEndpointAddress should update endpoint address") + public void setEndpointAddress_UpdatesEndpointAddress() { + // Arrange + DurableTaskSchedulerWorkerOptions options = new DurableTaskSchedulerWorkerOptions(); + + // Act + DurableTaskSchedulerWorkerOptions result = options.setEndpointAddress(VALID_ENDPOINT); + + // Assert + assertEquals(VALID_ENDPOINT, options.getEndpointAddress()); + assertSame(options, result); // Builder pattern returns this + } + + @Test + @DisplayName("setTaskHubName should update task hub name") + public void setTaskHubName_UpdatesTaskHubName() { + // Arrange + DurableTaskSchedulerWorkerOptions options = new DurableTaskSchedulerWorkerOptions(); + + // Act + DurableTaskSchedulerWorkerOptions result = options.setTaskHubName(VALID_TASKHUB); + + // Assert + assertEquals(VALID_TASKHUB, options.getTaskHubName()); + assertSame(options, result); // Builder pattern returns this + } + + @Test + @DisplayName("setCredential should update credential") + public void setCredential_UpdatesCredential() { + // Arrange + DurableTaskSchedulerWorkerOptions options = new DurableTaskSchedulerWorkerOptions(); + + // Act + DurableTaskSchedulerWorkerOptions result = options.setCredential(mockCredential); + + // Assert + assertSame(mockCredential, options.getCredential()); + assertSame(options, result); // Builder pattern returns this + } + + @Test + @DisplayName("setResourceId should update resource ID") + public void setResourceId_UpdatesResourceId() { + // Arrange + DurableTaskSchedulerWorkerOptions options = new DurableTaskSchedulerWorkerOptions(); + String customResourceId = "https://custom-resource.example.com"; + + // Act + DurableTaskSchedulerWorkerOptions result = options.setResourceId(customResourceId); + + // Assert + assertEquals(customResourceId, options.getResourceId()); + assertSame(options, result); // Builder pattern returns this + } + + @Test + @DisplayName("setAllowInsecureCredentials should update allowInsecureCredentials") + public void setAllowInsecureCredentials_UpdatesAllowInsecureCredentials() { + // Arrange + DurableTaskSchedulerWorkerOptions options = new DurableTaskSchedulerWorkerOptions(); + + // Act + DurableTaskSchedulerWorkerOptions result = options.setAllowInsecureCredentials(true); + + // Assert + assertTrue(options.isAllowInsecureCredentials()); + assertSame(options, result); // Builder pattern returns this + } + + @Test + @DisplayName("setTokenRefreshMargin should update token refresh margin") + public void setTokenRefreshMargin_UpdatesTokenRefreshMargin() { + // Arrange + DurableTaskSchedulerWorkerOptions options = new DurableTaskSchedulerWorkerOptions(); + Duration customMargin = Duration.ofMinutes(10); + + // Act + DurableTaskSchedulerWorkerOptions result = options.setTokenRefreshMargin(customMargin); + + // Assert + assertEquals(customMargin, options.getTokenRefreshMargin()); + assertSame(options, result); // Builder pattern returns this + } + + @Test + @DisplayName("validate should not throw when options are valid") + public void validate_DoesNotThrowWhenOptionsAreValid() { + // Arrange + DurableTaskSchedulerWorkerOptions options = new DurableTaskSchedulerWorkerOptions() + .setEndpointAddress(VALID_ENDPOINT) + .setTaskHubName(VALID_TASKHUB); + + // Act & Assert + assertDoesNotThrow(() -> options.validate()); + } + + @Test + @DisplayName("validate should throw when endpoint address is null") + public void validate_ThrowsWhenEndpointAddressIsNull() { + // Arrange + DurableTaskSchedulerWorkerOptions options = new DurableTaskSchedulerWorkerOptions() + .setEndpointAddress(null) + .setTaskHubName(VALID_TASKHUB); + + // Act & Assert + assertThrows(NullPointerException.class, () -> options.validate()); + } + + @Test + @DisplayName("validate should throw when task hub name is null") + public void validate_ThrowsWhenTaskHubNameIsNull() { + // Arrange + DurableTaskSchedulerWorkerOptions options = new DurableTaskSchedulerWorkerOptions() + .setEndpointAddress(VALID_ENDPOINT) + .setTaskHubName(null); + + // Act & Assert + assertThrows(NullPointerException.class, () -> options.validate()); + } + + @Test + @DisplayName("createGrpcChannel should create a channel") + public void createGrpcChannel_CreatesChannel() { + // Arrange + DurableTaskSchedulerWorkerOptions options = new DurableTaskSchedulerWorkerOptions() + .setEndpointAddress(VALID_ENDPOINT) + .setTaskHubName(VALID_TASKHUB); + + // Act + Channel channel = options.createGrpcChannel(); + + // Assert + assertNotNull(channel); + } + + @Test + @DisplayName("createGrpcChannel should handle endpoints without protocol") + public void createGrpcChannel_HandlesEndpointsWithoutProtocol() { + // Arrange + DurableTaskSchedulerWorkerOptions options = new DurableTaskSchedulerWorkerOptions() + .setEndpointAddress("example.com") + .setTaskHubName(VALID_TASKHUB); + + // Act & Assert + assertDoesNotThrow(() -> options.createGrpcChannel()); + } + + @Test + @DisplayName("createGrpcChannel should throw for invalid URLs") + public void createGrpcChannel_ThrowsForInvalidUrls() { + // Arrange + DurableTaskSchedulerWorkerOptions options = new DurableTaskSchedulerWorkerOptions() + .setEndpointAddress("invalid:url") + .setTaskHubName(VALID_TASKHUB); + + // Act & Assert + assertThrows(IllegalArgumentException.class, () -> options.createGrpcChannel()); + } +} \ No newline at end of file From f59af6b24775e737498d0fabc7a14fdee2c41b95 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 20 Mar 2025 16:31:33 -0700 Subject: [PATCH 39/53] Update Gradle build command to include stacktrace option for better debugging --- .github/workflows/build-validation.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-validation.yml b/.github/workflows/build-validation.yml index 20635b43..f0e104e7 100644 --- a/.github/workflows/build-validation.yml +++ b/.github/workflows/build-validation.yml @@ -42,7 +42,8 @@ jobs: uses: gradle/gradle-build-action@v2 - name: Build with Gradle - run: ./gradlew build + run: ./gradlew build --stacktrace + # TODO: Move the sidecar into a central image repository - name: Initialize Durable Task Sidecar From f7c53769356f3dcdbbe09bcd65359a7a80f6ec19 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 20 Mar 2025 16:42:30 -0700 Subject: [PATCH 40/53] cleanup --- .../test/java/com/microsoft/durabletask/IntegrationTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/test/java/com/microsoft/durabletask/IntegrationTests.java b/client/src/test/java/com/microsoft/durabletask/IntegrationTests.java index 5e9ac5d9..467151d6 100644 --- a/client/src/test/java/com/microsoft/durabletask/IntegrationTests.java +++ b/client/src/test/java/com/microsoft/durabletask/IntegrationTests.java @@ -556,7 +556,7 @@ void restartOrchestrationThrowsException() { } -// @Test + @Test void suspendResumeOrchestration() throws TimeoutException, InterruptedException { final String orchestratorName = "suspend"; final String eventName = "MyEvent"; From 538033c834e620574b5ab44b697f984ff4e4de89 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 20 Mar 2025 16:54:32 -0700 Subject: [PATCH 41/53] Enhance build validation workflow to handle build failures by uploading test reports and failing the job if necessary --- .github/workflows/build-validation.yml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-validation.yml b/.github/workflows/build-validation.yml index f0e104e7..6e5c6cfc 100644 --- a/.github/workflows/build-validation.yml +++ b/.github/workflows/build-validation.yml @@ -42,8 +42,20 @@ jobs: uses: gradle/gradle-build-action@v2 - name: Build with Gradle - run: ./gradlew build --stacktrace + run: ./gradlew build || echo "BUILD_FAILED=true" >> $GITHUB_ENV + continue-on-error: true + - name: Upload test reports if tests failed + if: env.BUILD_FAILED == 'true' + uses: actions/upload-artifact@v4 + with: + name: Unit Test Reports + path: '**/build/reports/tests/test' + if-no-files-found: ignore # Prevents errors if no reports exist + + - name: Fail the job if build failed + if: env.BUILD_FAILED == 'true' + run: exit 1 # TODO: Move the sidecar into a central image repository - name: Initialize Durable Task Sidecar From bb1fbb176f3f8cffe31015552cac739a8aa76bb8 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 20 Mar 2025 17:17:11 -0700 Subject: [PATCH 42/53] Update PATH_TO_TEST_JAVA_RUNTIME in build.gradle files to use environment variable directly for consistency across modules --- azuremanaged/client/build.gradle | 2 +- azuremanaged/shared/build.gradle | 2 +- azuremanaged/worker/build.gradle | 2 +- client/build.gradle | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/azuremanaged/client/build.gradle b/azuremanaged/client/build.gradle index cf760fef..d6b0b41a 100644 --- a/azuremanaged/client/build.gradle +++ b/azuremanaged/client/build.gradle @@ -22,7 +22,7 @@ def azureIdentityVersion = '1.11.1' def durabletaskClientVersion = '1.5.0' // When build on local, you need to set this value to your local jdk11 directory. // Java11 is used to compile and run all the tests. -def PATH_TO_TEST_JAVA_RUNTIME = System.env.JDK_11 ?: System.getProperty("java.home") +def PATH_TO_TEST_JAVA_RUNTIME = "$System.env.JDK_11" repositories { mavenCentral() diff --git a/azuremanaged/shared/build.gradle b/azuremanaged/shared/build.gradle index 1285ab1d..95037f58 100644 --- a/azuremanaged/shared/build.gradle +++ b/azuremanaged/shared/build.gradle @@ -13,7 +13,7 @@ def azureCoreVersion = '1.45.0' def azureIdentityVersion = '1.11.1' // When build on local, you need to set this value to your local jdk11 directory. // Java11 is used to compile and run all the tests. -def PATH_TO_TEST_JAVA_RUNTIME = System.env.JDK_11 ?: System.getProperty("java.home") +def PATH_TO_TEST_JAVA_RUNTIME = "$System.env.JDK_11" repositories { mavenCentral() diff --git a/azuremanaged/worker/build.gradle b/azuremanaged/worker/build.gradle index 2a8b2df4..1ec14e91 100644 --- a/azuremanaged/worker/build.gradle +++ b/azuremanaged/worker/build.gradle @@ -22,7 +22,7 @@ def azureIdentityVersion = '1.11.1' def durabletaskClientVersion = '1.5.0' // When build on local, you need to set this value to your local jdk11 directory. // Java11 is used to compile and run all the tests. -def PATH_TO_TEST_JAVA_RUNTIME = System.env.JDK_11 ?: System.getProperty("java.home") +def PATH_TO_TEST_JAVA_RUNTIME = "$System.env.JDK_11" repositories { mavenCentral() diff --git a/client/build.gradle b/client/build.gradle index 98ff6adc..822265b5 100644 --- a/client/build.gradle +++ b/client/build.gradle @@ -17,7 +17,7 @@ def jacksonVersion = '2.15.3' // When build on local, you need to set this value to your local jdk11 directory. // Java11 is used to compile and run all the tests. // Example for Windows: C:/Program Files/Java/openjdk-11.0.12_7/ -def PATH_TO_TEST_JAVA_RUNTIME = System.env.JDK_11 ?: System.getProperty("java.home") +def PATH_TO_TEST_JAVA_RUNTIME = "$System.env.JDK_11" dependencies { From 637863c0e3674bdc0405b0abc3aa597ad23e19b7 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 20 Mar 2025 17:20:02 -0700 Subject: [PATCH 43/53] Revert "Update PATH_TO_TEST_JAVA_RUNTIME in build.gradle files to use environment variable directly for consistency across modules" This reverts commit bb1fbb176f3f8cffe31015552cac739a8aa76bb8. --- azuremanaged/client/build.gradle | 2 +- azuremanaged/shared/build.gradle | 2 +- azuremanaged/worker/build.gradle | 2 +- client/build.gradle | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/azuremanaged/client/build.gradle b/azuremanaged/client/build.gradle index d6b0b41a..cf760fef 100644 --- a/azuremanaged/client/build.gradle +++ b/azuremanaged/client/build.gradle @@ -22,7 +22,7 @@ def azureIdentityVersion = '1.11.1' def durabletaskClientVersion = '1.5.0' // When build on local, you need to set this value to your local jdk11 directory. // Java11 is used to compile and run all the tests. -def PATH_TO_TEST_JAVA_RUNTIME = "$System.env.JDK_11" +def PATH_TO_TEST_JAVA_RUNTIME = System.env.JDK_11 ?: System.getProperty("java.home") repositories { mavenCentral() diff --git a/azuremanaged/shared/build.gradle b/azuremanaged/shared/build.gradle index 95037f58..1285ab1d 100644 --- a/azuremanaged/shared/build.gradle +++ b/azuremanaged/shared/build.gradle @@ -13,7 +13,7 @@ def azureCoreVersion = '1.45.0' def azureIdentityVersion = '1.11.1' // When build on local, you need to set this value to your local jdk11 directory. // Java11 is used to compile and run all the tests. -def PATH_TO_TEST_JAVA_RUNTIME = "$System.env.JDK_11" +def PATH_TO_TEST_JAVA_RUNTIME = System.env.JDK_11 ?: System.getProperty("java.home") repositories { mavenCentral() diff --git a/azuremanaged/worker/build.gradle b/azuremanaged/worker/build.gradle index 1ec14e91..2a8b2df4 100644 --- a/azuremanaged/worker/build.gradle +++ b/azuremanaged/worker/build.gradle @@ -22,7 +22,7 @@ def azureIdentityVersion = '1.11.1' def durabletaskClientVersion = '1.5.0' // When build on local, you need to set this value to your local jdk11 directory. // Java11 is used to compile and run all the tests. -def PATH_TO_TEST_JAVA_RUNTIME = "$System.env.JDK_11" +def PATH_TO_TEST_JAVA_RUNTIME = System.env.JDK_11 ?: System.getProperty("java.home") repositories { mavenCentral() diff --git a/client/build.gradle b/client/build.gradle index 822265b5..98ff6adc 100644 --- a/client/build.gradle +++ b/client/build.gradle @@ -17,7 +17,7 @@ def jacksonVersion = '2.15.3' // When build on local, you need to set this value to your local jdk11 directory. // Java11 is used to compile and run all the tests. // Example for Windows: C:/Program Files/Java/openjdk-11.0.12_7/ -def PATH_TO_TEST_JAVA_RUNTIME = "$System.env.JDK_11" +def PATH_TO_TEST_JAVA_RUNTIME = System.env.JDK_11 ?: System.getProperty("java.home") dependencies { From 8d530cb9bf5d49db798ab285bbb878cde61be663 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 20 Mar 2025 17:30:58 -0700 Subject: [PATCH 44/53] separate build and unit tests --- .github/workflows/build-validation.yml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-validation.yml b/.github/workflows/build-validation.yml index 6e5c6cfc..1c33334d 100644 --- a/.github/workflows/build-validation.yml +++ b/.github/workflows/build-validation.yml @@ -42,19 +42,22 @@ jobs: uses: gradle/gradle-build-action@v2 - name: Build with Gradle - run: ./gradlew build || echo "BUILD_FAILED=true" >> $GITHUB_ENV - continue-on-error: true + run: ./gradlew build -x test + - name: Run Unit Tests with Gradle + run: ./gradlew clean test || echo "UNIT_TEST_FAILED=true" >> $GITHUB_ENV + continue-on-error: true + - name: Upload test reports if tests failed - if: env.BUILD_FAILED == 'true' + if: env.UNIT_TEST_FAILED == 'true' uses: actions/upload-artifact@v4 with: name: Unit Test Reports path: '**/build/reports/tests/test' if-no-files-found: ignore # Prevents errors if no reports exist - - name: Fail the job if build failed - if: env.BUILD_FAILED == 'true' + - name: Fail the job if unit tests failed + if: env.UNIT_TEST_FAILED == 'true' run: exit 1 # TODO: Move the sidecar into a central image repository From 5f3ebfb5a4a59a27989e13ca6cb9af910766ec06 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 20 Mar 2025 17:31:30 -0700 Subject: [PATCH 45/53] use jdk11 for unit tests --- .github/workflows/build-validation.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-validation.yml b/.github/workflows/build-validation.yml index 1c33334d..df14fc6a 100644 --- a/.github/workflows/build-validation.yml +++ b/.github/workflows/build-validation.yml @@ -45,7 +45,9 @@ jobs: run: ./gradlew build -x test - name: Run Unit Tests with Gradle - run: ./gradlew clean test || echo "UNIT_TEST_FAILED=true" >> $GITHUB_ENV + run: | + export JAVA_HOME=$JDK_11 + ./gradlew clean test || echo "UNIT_TEST_FAILED=true" >> $GITHUB_ENV continue-on-error: true - name: Upload test reports if tests failed From 02ec66d119317ffeef0b2dffa925692fc8464270 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 21 Mar 2025 08:28:34 -0700 Subject: [PATCH 46/53] update sample --- .../WebAppToDurableTaskSchedulerSample.java | 14 +---- samples/src/main/resources/order-api.http | 60 +++++++++++++++++++ 2 files changed, 62 insertions(+), 12 deletions(-) create mode 100644 samples/src/main/resources/order-api.http diff --git a/samples/src/main/java/io/durabletask/samples/WebAppToDurableTaskSchedulerSample.java b/samples/src/main/java/io/durabletask/samples/WebAppToDurableTaskSchedulerSample.java index bd1e08e8..8cb887c6 100644 --- a/samples/src/main/java/io/durabletask/samples/WebAppToDurableTaskSchedulerSample.java +++ b/samples/src/main/java/io/durabletask/samples/WebAppToDurableTaskSchedulerSample.java @@ -190,18 +190,8 @@ public String createOrder(@RequestBody String orderJson) throws Exception { orderJson ); - // Wait for the orchestration to complete with a timeout - OrchestrationMetadata metadata = client.waitForInstanceCompletion( - instanceId, - Duration.ofSeconds(30), - true - ); - - if (metadata.getRuntimeStatus() == OrchestrationRuntimeStatus.COMPLETED) { - return metadata.readOutputAs(String.class); - } else { - return "{\"status\": \"" + metadata.getRuntimeStatus() + "\"}"; - } + // Return the instance ID immediately without waiting for completion + return "{\"instanceId\": \"" + instanceId + "\"}"; } @GetMapping("/{instanceId}") diff --git a/samples/src/main/resources/order-api.http b/samples/src/main/resources/order-api.http new file mode 100644 index 00000000..71d23162 --- /dev/null +++ b/samples/src/main/resources/order-api.http @@ -0,0 +1,60 @@ +### Variables +@instanceId = dafc6c87-964b-4e99-8aaf-a5d1061de890 + +### Create a new order +POST http://localhost:8082/api/orders +Content-Type: application/json + +{ + "orderId": "ORD123456", + "customerId": "CUST789", + "amount": 125.50, + "items": [ + { + "productId": "PROD001", + "quantity": 2, + "price": 49.99 + }, + { + "productId": "PROD002", + "quantity": 1, + "price": 25.52 + } + ], + "shippingAddress": { + "street": "123 Main St", + "city": "Seattle", + "state": "WA", + "zipCode": "98101", + "country": "USA" + } +} + +### Get order by instance ID +# Replace the instanceId variable with the actual value returned from the create order request +GET http://localhost:8082/api/orders/{{instanceId}} +Content-Type: application/json + +### Example with invalid order (amount is 0) +POST http://localhost:8082/api/orders +Content-Type: application/json + +{ + "orderId": "ORD123457", + "customerId": "CUST789", + "amount": 0, + "items": [ + { + "productId": "PROD003", + "quantity": 1, + "price": 0 + } + ], + "shippingAddress": { + "street": "123 Main St", + "city": "Seattle", + "state": "WA", + "zipCode": "98101", + "country": "USA" + } +} \ No newline at end of file From f23edcc10e367c2d54e0ff17c9749037809c225d Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 21 Mar 2025 08:29:33 -0700 Subject: [PATCH 47/53] clean up --- ...der-api.http => web-app-to-durable-task-scheduler-sample.http} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename samples/src/main/resources/{order-api.http => web-app-to-durable-task-scheduler-sample.http} (100%) diff --git a/samples/src/main/resources/order-api.http b/samples/src/main/resources/web-app-to-durable-task-scheduler-sample.http similarity index 100% rename from samples/src/main/resources/order-api.http rename to samples/src/main/resources/web-app-to-durable-task-scheduler-sample.http From 299ab73421c7e1fd0488e76f2703110ddd425aff Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 21 Mar 2025 11:25:47 -0700 Subject: [PATCH 48/53] rename azuremanaged module names to avoid colliding module names named "client", gradle expects unique module names and build one module only if colliding names --- .../{client => dtclient}/build.gradle | 3 +- .../DurableTaskSchedulerClientExtensions.java | 0 .../DurableTaskSchedulerClientOptions.java | 0 ...ableTaskSchedulerClientExtensionsTest.java | 0 ...DurableTaskSchedulerClientOptionsTest.java | 0 .../{worker => dtworker}/build.gradle | 3 +- .../DurableTaskSchedulerWorkerExtensions.java | 0 .../DurableTaskSchedulerWorkerOptions.java | 0 ...ableTaskSchedulerWorkerExtensionsTest.java | 0 ...DurableTaskSchedulerWorkerOptionsTest.java | 0 .../durabletask/DurableTaskGrpcWorker.java | 4 +++ .../PROTO_SOURCE_COMMIT_HASH | 2 +- .../protos/orchestrator_service.proto | 33 +++++++++++++++++++ samples/build.gradle | 9 ++--- settings.gradle | 4 +-- 15 files changed, 49 insertions(+), 9 deletions(-) rename azuremanaged/{client => dtclient}/build.gradle (97%) rename azuremanaged/{client => dtclient}/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientExtensions.java (100%) rename azuremanaged/{client => dtclient}/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientOptions.java (100%) rename azuremanaged/{client => dtclient}/src/test/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientExtensionsTest.java (100%) rename azuremanaged/{client => dtclient}/src/test/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientOptionsTest.java (100%) rename azuremanaged/{worker => dtworker}/build.gradle (96%) rename azuremanaged/{worker => dtworker}/src/main/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerExtensions.java (100%) rename azuremanaged/{worker => dtworker}/src/main/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerOptions.java (100%) rename azuremanaged/{worker => dtworker}/src/test/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerExtensionsTest.java (100%) rename azuremanaged/{worker => dtworker}/src/test/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerOptionsTest.java (100%) diff --git a/azuremanaged/client/build.gradle b/azuremanaged/dtclient/build.gradle similarity index 97% rename from azuremanaged/client/build.gradle rename to azuremanaged/dtclient/build.gradle index cf760fef..587863ad 100644 --- a/azuremanaged/client/build.gradle +++ b/azuremanaged/dtclient/build.gradle @@ -29,12 +29,13 @@ repositories { } dependencies { + implementation project(':client') implementation project(':azuremanaged:shared') implementation "io.grpc:grpc-protobuf:${grpcVersion}" implementation "io.grpc:grpc-stub:${grpcVersion}" runtimeOnly "io.grpc:grpc-netty-shaded:${grpcVersion}" - implementation "com.microsoft:durabletask-client:${durabletaskClientVersion}" + // implementation "com.microsoft:durabletask-client:${durabletaskClientVersion}" implementation "com.azure:azure-core:${azureCoreVersion}" implementation "com.azure:azure-identity:${azureIdentityVersion}" diff --git a/azuremanaged/client/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientExtensions.java b/azuremanaged/dtclient/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientExtensions.java similarity index 100% rename from azuremanaged/client/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientExtensions.java rename to azuremanaged/dtclient/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientExtensions.java diff --git a/azuremanaged/client/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientOptions.java b/azuremanaged/dtclient/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientOptions.java similarity index 100% rename from azuremanaged/client/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientOptions.java rename to azuremanaged/dtclient/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientOptions.java diff --git a/azuremanaged/client/src/test/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientExtensionsTest.java b/azuremanaged/dtclient/src/test/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientExtensionsTest.java similarity index 100% rename from azuremanaged/client/src/test/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientExtensionsTest.java rename to azuremanaged/dtclient/src/test/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientExtensionsTest.java diff --git a/azuremanaged/client/src/test/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientOptionsTest.java b/azuremanaged/dtclient/src/test/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientOptionsTest.java similarity index 100% rename from azuremanaged/client/src/test/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientOptionsTest.java rename to azuremanaged/dtclient/src/test/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientOptionsTest.java diff --git a/azuremanaged/worker/build.gradle b/azuremanaged/dtworker/build.gradle similarity index 96% rename from azuremanaged/worker/build.gradle rename to azuremanaged/dtworker/build.gradle index 2a8b2df4..1fd03b15 100644 --- a/azuremanaged/worker/build.gradle +++ b/azuremanaged/dtworker/build.gradle @@ -30,9 +30,10 @@ repositories { dependencies { implementation project(':azuremanaged:shared') + implementation project(':client') implementation "io.grpc:grpc-protobuf:${grpcVersion}" implementation "io.grpc:grpc-stub:${grpcVersion}" - implementation "com.microsoft:durabletask-client:${durabletaskClientVersion}" + // implementation "com.microsoft:durabletask-client:${durabletaskClientVersion}" runtimeOnly "io.grpc:grpc-netty-shaded:${grpcVersion}" implementation "com.azure:azure-core:${azureCoreVersion}" implementation "com.azure:azure-identity:${azureIdentityVersion}" diff --git a/azuremanaged/worker/src/main/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerExtensions.java b/azuremanaged/dtworker/src/main/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerExtensions.java similarity index 100% rename from azuremanaged/worker/src/main/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerExtensions.java rename to azuremanaged/dtworker/src/main/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerExtensions.java diff --git a/azuremanaged/worker/src/main/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerOptions.java b/azuremanaged/dtworker/src/main/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerOptions.java similarity index 100% rename from azuremanaged/worker/src/main/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerOptions.java rename to azuremanaged/dtworker/src/main/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerOptions.java diff --git a/azuremanaged/worker/src/test/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerExtensionsTest.java b/azuremanaged/dtworker/src/test/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerExtensionsTest.java similarity index 100% rename from azuremanaged/worker/src/test/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerExtensionsTest.java rename to azuremanaged/dtworker/src/test/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerExtensionsTest.java diff --git a/azuremanaged/worker/src/test/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerOptionsTest.java b/azuremanaged/dtworker/src/test/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerOptionsTest.java similarity index 100% rename from azuremanaged/worker/src/test/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerOptionsTest.java rename to azuremanaged/dtworker/src/test/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerOptionsTest.java diff --git a/client/src/main/java/com/microsoft/durabletask/DurableTaskGrpcWorker.java b/client/src/main/java/com/microsoft/durabletask/DurableTaskGrpcWorker.java index 94354e29..89f87a9f 100644 --- a/client/src/main/java/com/microsoft/durabletask/DurableTaskGrpcWorker.java +++ b/client/src/main/java/com/microsoft/durabletask/DurableTaskGrpcWorker.java @@ -177,6 +177,10 @@ public void startAndBlock() { } this.sidecarClient.completeActivityTask(responseBuilder.build()); + } + else if (requestType == RequestCase.HEALTHPING) + { + // No-op } else { logger.log(Level.WARNING, "Received and dropped an unknown '{0}' work-item from the sidecar.", requestType); } diff --git a/internal/durabletask-protobuf/PROTO_SOURCE_COMMIT_HASH b/internal/durabletask-protobuf/PROTO_SOURCE_COMMIT_HASH index e409e9a4..a08790e4 100644 --- a/internal/durabletask-protobuf/PROTO_SOURCE_COMMIT_HASH +++ b/internal/durabletask-protobuf/PROTO_SOURCE_COMMIT_HASH @@ -1 +1 @@ -4792f47019ab2b3e9ea979fb4af72427a4144c51 \ No newline at end of file +b1e9c96572a9fa0b81829fe0e976cd39de91877e \ No newline at end of file diff --git a/internal/durabletask-protobuf/protos/orchestrator_service.proto b/internal/durabletask-protobuf/protos/orchestrator_service.proto index 64e75281..0f7d8220 100644 --- a/internal/durabletask-protobuf/protos/orchestrator_service.proto +++ b/internal/durabletask-protobuf/protos/orchestrator_service.proto @@ -617,6 +617,30 @@ message StartNewOrchestrationAction { google.protobuf.Timestamp scheduledTime = 5; } +message AbandonActivityTaskRequest { + string completionToken = 1; +} + +message AbandonActivityTaskResponse { + // Empty. +} + +message AbandonOrchestrationTaskRequest { + string completionToken = 1; +} + +message AbandonOrchestrationTaskResponse { + // Empty. +} + +message AbandonEntityTaskRequest { + string completionToken = 1; +} + +message AbandonEntityTaskResponse { + // Empty. +} + service TaskHubSidecarService { // Sends a hello request to the sidecar service. rpc Hello(google.protobuf.Empty) returns (google.protobuf.Empty); @@ -678,6 +702,15 @@ service TaskHubSidecarService { // clean entity storage rpc CleanEntityStorage(CleanEntityStorageRequest) returns (CleanEntityStorageResponse); + + // Abandons a single work item + rpc AbandonTaskActivityWorkItem(AbandonActivityTaskRequest) returns (AbandonActivityTaskResponse); + + // Abandon an orchestration work item + rpc AbandonTaskOrchestratorWorkItem(AbandonOrchestrationTaskRequest) returns (AbandonOrchestrationTaskResponse); + + // Abandon an entity work item + rpc AbandonTaskEntityWorkItem(AbandonEntityTaskRequest) returns (AbandonEntityTaskResponse); } message GetWorkItemsRequest { diff --git a/samples/build.gradle b/samples/build.gradle index efff1293..9479bfbf 100644 --- a/samples/build.gradle +++ b/samples/build.gradle @@ -15,10 +15,11 @@ application { } dependencies { - implementation project(':azuremanaged:client') - implementation project(':azuremanaged:worker') - - implementation "com.microsoft:durabletask-client:${durabletaskClientVersion}" + implementation project(':client') + implementation project(':azuremanaged:dtclient') + implementation project(':azuremanaged:dtworker') + + // implementation "com.microsoft:durabletask-client:${durabletaskClientVersion}" implementation 'org.springframework.boot:spring-boot-starter-web' implementation platform("org.springframework.boot:spring-boot-dependencies:2.5.2") implementation 'org.springframework.boot:spring-boot-starter' diff --git a/settings.gradle b/settings.gradle index f5a32602..ace62edc 100644 --- a/settings.gradle +++ b/settings.gradle @@ -3,8 +3,8 @@ rootProject.name = 'durabletask-java' include ":client" include ":azurefunctions" include ":azuremanaged" -include ":azuremanaged:client" -include ":azuremanaged:worker" +include ":azuremanaged:dtclient" +include ":azuremanaged:dtworker" include ":azuremanaged:shared" include ":samples" include ":samples-azure-functions" From 8cf6cdbcf2d3f216d3d72ab7a64f332ae014bcaa Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 21 Mar 2025 15:40:00 -0700 Subject: [PATCH 49/53] update azure managed to one package --- azuremanaged/build.gradle | 120 +++++++++++++++++- azuremanaged/dtclient/build.gradle | 117 ----------------- azuremanaged/dtworker/build.gradle | 116 ----------------- azuremanaged/settings.gradle | 5 - azuremanaged/shared/build.gradle | 56 -------- .../azuremanaged/AccessTokenCache.java | 2 +- .../DurableTaskSchedulerClientExtensions.java | 2 +- .../DurableTaskSchedulerClientOptions.java | 4 +- .../DurableTaskSchedulerConnectionString.java | 2 +- .../DurableTaskSchedulerWorkerExtensions.java | 2 +- .../DurableTaskSchedulerWorkerOptions.java | 4 +- .../azuremanaged/AccessTokenCacheTest.java | 2 +- ...ableTaskSchedulerClientExtensionsTest.java | 2 +- ...DurableTaskSchedulerClientOptionsTest.java | 4 +- ...ableTaskSchedulerConnectionStringTest.java | 2 +- ...ableTaskSchedulerWorkerExtensionsTest.java | 2 +- ...DurableTaskSchedulerWorkerOptionsTest.java | 4 +- samples/build.gradle | 5 +- .../WebAppToDurableTaskSchedulerSample.java | 4 +- settings.gradle | 3 - 20 files changed, 133 insertions(+), 325 deletions(-) delete mode 100644 azuremanaged/dtclient/build.gradle delete mode 100644 azuremanaged/dtworker/build.gradle delete mode 100644 azuremanaged/settings.gradle delete mode 100644 azuremanaged/shared/build.gradle rename azuremanaged/{shared/src/main/java/com/microsoft/durabletask/shared => src/main/java/com/microsoft/durabletask}/azuremanaged/AccessTokenCache.java (96%) rename azuremanaged/{dtclient/src/main/java/com/microsoft/durabletask/client => src/main/java/com/microsoft/durabletask}/azuremanaged/DurableTaskSchedulerClientExtensions.java (98%) rename azuremanaged/{dtclient/src/main/java/com/microsoft/durabletask/client => src/main/java/com/microsoft/durabletask}/azuremanaged/DurableTaskSchedulerClientOptions.java (97%) rename azuremanaged/{shared/src/main/java/com/microsoft/durabletask/shared => src/main/java/com/microsoft/durabletask}/azuremanaged/DurableTaskSchedulerConnectionString.java (98%) rename azuremanaged/{dtworker/src/main/java/com/microsoft/durabletask/worker => src/main/java/com/microsoft/durabletask}/azuremanaged/DurableTaskSchedulerWorkerExtensions.java (98%) rename azuremanaged/{dtworker/src/main/java/com/microsoft/durabletask/worker => src/main/java/com/microsoft/durabletask}/azuremanaged/DurableTaskSchedulerWorkerOptions.java (97%) rename azuremanaged/{shared/src/test/java/com/microsoft/durabletask/shared => src/test/java/com/microsoft/durabletask}/azuremanaged/AccessTokenCacheTest.java (98%) rename azuremanaged/{dtclient/src/test/java/com/microsoft/durabletask/client => src/test/java/com/microsoft/durabletask}/azuremanaged/DurableTaskSchedulerClientExtensionsTest.java (99%) rename azuremanaged/{dtclient/src/test/java/com/microsoft/durabletask/client => src/test/java/com/microsoft/durabletask}/azuremanaged/DurableTaskSchedulerClientOptionsTest.java (98%) rename azuremanaged/{shared/src/test/java/com/microsoft/durabletask/shared => src/test/java/com/microsoft/durabletask}/azuremanaged/DurableTaskSchedulerConnectionStringTest.java (99%) rename azuremanaged/{dtworker/src/test/java/com/microsoft/durabletask/worker => src/test/java/com/microsoft/durabletask}/azuremanaged/DurableTaskSchedulerWorkerExtensionsTest.java (99%) rename azuremanaged/{dtworker/src/test/java/com/microsoft/durabletask/worker => src/test/java/com/microsoft/durabletask}/azuremanaged/DurableTaskSchedulerWorkerOptionsTest.java (98%) diff --git a/azuremanaged/build.gradle b/azuremanaged/build.gradle index c3ecbc84..97e09a6b 100644 --- a/azuremanaged/build.gradle +++ b/azuremanaged/build.gradle @@ -1,4 +1,116 @@ -allprojects { - group = 'com.microsoft' - version = '1.5.0' -} \ No newline at end of file +/* + * This build file was generated by the Gradle 'init' task. + * + * This generated file contains a sample Java project to get you started. + * For more details take a look at the Java Quickstart chapter in the Gradle + * user guide available at https://docs.gradle.org/4.4.1/userguide/tutorial_java_projects.html + */ + +plugins { + id 'java' + id 'com.google.protobuf' version '0.8.16' + id 'idea' + id 'maven-publish' + id 'signing' +} + +archivesBaseName = 'durabletask-azuremanaged' +group 'com.microsoft' +version = '1.5.0' + +def grpcVersion = '1.59.0' +def azureCoreVersion = '1.45.0' +def azureIdentityVersion = '1.11.1' +// When build on local, you need to set this value to your local jdk11 directory. +// Java11 is used to compile and run all the tests. +def PATH_TO_TEST_JAVA_RUNTIME = System.env.JDK_11 ?: System.getProperty("java.home") + +repositories { + mavenCentral() +} + +dependencies { + implementation project(':client') + implementation "io.grpc:grpc-protobuf:${grpcVersion}" + implementation "io.grpc:grpc-stub:${grpcVersion}" + runtimeOnly "io.grpc:grpc-netty-shaded:${grpcVersion}" + + implementation "com.azure:azure-core:${azureCoreVersion}" + implementation "com.azure:azure-identity:${azureIdentityVersion}" + + // Test dependencies + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.0' + testImplementation 'org.junit.jupiter:junit-jupiter-params:5.10.0' + testImplementation 'org.mockito:mockito-core:5.3.1' + testImplementation 'org.mockito:mockito-junit-jupiter:5.3.1' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.10.0' +} + +compileJava { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +compileTestJava { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + options.fork = true + options.forkOptions.executable = "${PATH_TO_TEST_JAVA_RUNTIME}/bin/javac" +} + +test { + useJUnitPlatform() +} + +publishing { + repositories { + maven { + url "file://$project.rootDir/repo" + } + } + publications { + mavenJava(MavenPublication) { + from components.java + artifactId = archivesBaseName + pom { + name = 'Durable Task Azure Managed SDK for Java' + description = 'This package contains classes and interfaces for building Durable Task orchestrations in Java using Azure Managed mode.' + url = "https://github.com/microsoft/durabletask-java/tree/main/azuremanaged" + licenses { + license { + name = "MIT License" + url = "https://opensource.org/licenses/MIT" + distribution = "repo" + } + } + developers { + developer { + id = "Microsoft" + name = "Microsoft Corporation" + } + } + scm { + connection = "scm:git:https://github.com/microsoft/durabletask-java" + developerConnection = "scm:git:git@github.com:microsoft/durabletask-java" + url = "https://github.com/microsoft/durabletask-java/tree/main/azuremanaged" + } + withXml { + project.configurations.compileOnly.allDependencies.each { dependency -> + asNode().dependencies[0].appendNode("dependency").with { + it.appendNode("groupId", dependency.group) + it.appendNode("artifactId", dependency.name) + it.appendNode("version", dependency.version) + it.appendNode("scope", "provided") + } + } + } + } + } + } +} + +java { + withSourcesJar() + withJavadocJar() +} + diff --git a/azuremanaged/dtclient/build.gradle b/azuremanaged/dtclient/build.gradle deleted file mode 100644 index 587863ad..00000000 --- a/azuremanaged/dtclient/build.gradle +++ /dev/null @@ -1,117 +0,0 @@ -/* - * This build file was generated by the Gradle 'init' task. - * - * This generated file contains a sample Java project to get you started. - * For more details take a look at the Java Quickstart chapter in the Gradle - * user guide available at https://docs.gradle.org/4.4.1/userguide/tutorial_java_projects.html - */ - -plugins { - id 'java' - id 'com.google.protobuf' version '0.8.16' - id 'idea' - id 'maven-publish' - id 'signing' -} - -archivesBaseName = 'durabletask-azuremanaged-client' - -def grpcVersion = '1.59.0' -def azureCoreVersion = '1.45.0' -def azureIdentityVersion = '1.11.1' -def durabletaskClientVersion = '1.5.0' -// When build on local, you need to set this value to your local jdk11 directory. -// Java11 is used to compile and run all the tests. -def PATH_TO_TEST_JAVA_RUNTIME = System.env.JDK_11 ?: System.getProperty("java.home") - -repositories { - mavenCentral() -} - -dependencies { - implementation project(':client') - implementation project(':azuremanaged:shared') - implementation "io.grpc:grpc-protobuf:${grpcVersion}" - implementation "io.grpc:grpc-stub:${grpcVersion}" - runtimeOnly "io.grpc:grpc-netty-shaded:${grpcVersion}" - - // implementation "com.microsoft:durabletask-client:${durabletaskClientVersion}" - implementation "com.azure:azure-core:${azureCoreVersion}" - implementation "com.azure:azure-identity:${azureIdentityVersion}" - - // Test dependencies - testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.0' - testImplementation 'org.junit.jupiter:junit-jupiter-params:5.10.0' - testImplementation 'org.mockito:mockito-core:5.3.1' - testImplementation 'org.mockito:mockito-junit-jupiter:5.3.1' - testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.10.0' -} - -compileJava { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} - -compileTestJava { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - options.fork = true - options.forkOptions.executable = "${PATH_TO_TEST_JAVA_RUNTIME}/bin/javac" -} - -test { - useJUnitPlatform() -} - -publishing { - repositories { - maven { - url "file://$project.rootDir/repo" - } - } - publications { - mavenJava(MavenPublication) { - from components.java - artifactId = archivesBaseName - pom { - name = 'Durable Task Azure Managed Client SDK for Java' - description = 'This package contains classes and interfaces for building Durable Task orchestrations in Java using Azure Managed mode.' - url = "https://github.com/microsoft/durabletask-java/tree/main/azuremanaged/client" - licenses { - license { - name = "MIT License" - url = "https://opensource.org/licenses/MIT" - distribution = "repo" - } - } - developers { - developer { - id = "Microsoft" - name = "Microsoft Corporation" - } - } - scm { - connection = "scm:git:https://github.com/microsoft/durabletask-java" - developerConnection = "scm:git:git@github.com:microsoft/durabletask-java" - url = "https://github.com/microsoft/durabletask-java/tree/main/azuremanaged/client" - } - withXml { - project.configurations.compileOnly.allDependencies.each { dependency -> - asNode().dependencies[0].appendNode("dependency").with { - it.appendNode("groupId", dependency.group) - it.appendNode("artifactId", dependency.name) - it.appendNode("version", dependency.version) - it.appendNode("scope", "provided") - } - } - } - } - } - } -} - -java { - withSourcesJar() - withJavadocJar() -} - diff --git a/azuremanaged/dtworker/build.gradle b/azuremanaged/dtworker/build.gradle deleted file mode 100644 index 1fd03b15..00000000 --- a/azuremanaged/dtworker/build.gradle +++ /dev/null @@ -1,116 +0,0 @@ -/* - * This build file was generated by the Gradle 'init' task. - * - * This generated file contains a sample Java project to get you started. - * For more details take a look at the Java Quickstart chapter in the Gradle - * user guide available at https://docs.gradle.org/4.4.1/userguide/tutorial_java_projects.html - */ - -plugins { - id 'java' - id 'com.google.protobuf' version '0.8.16' - id 'idea' - id 'maven-publish' - id 'signing' -} - -archivesBaseName = 'durabletask-azuremanaged-worker' - -def grpcVersion = '1.59.0' -def azureCoreVersion = '1.45.0' -def azureIdentityVersion = '1.11.1' -def durabletaskClientVersion = '1.5.0' -// When build on local, you need to set this value to your local jdk11 directory. -// Java11 is used to compile and run all the tests. -def PATH_TO_TEST_JAVA_RUNTIME = System.env.JDK_11 ?: System.getProperty("java.home") - -repositories { - mavenCentral() -} - -dependencies { - implementation project(':azuremanaged:shared') - implementation project(':client') - implementation "io.grpc:grpc-protobuf:${grpcVersion}" - implementation "io.grpc:grpc-stub:${grpcVersion}" - // implementation "com.microsoft:durabletask-client:${durabletaskClientVersion}" - runtimeOnly "io.grpc:grpc-netty-shaded:${grpcVersion}" - implementation "com.azure:azure-core:${azureCoreVersion}" - implementation "com.azure:azure-identity:${azureIdentityVersion}" - - // Test dependencies - testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.0' - testImplementation 'org.junit.jupiter:junit-jupiter-params:5.10.0' - testImplementation 'org.mockito:mockito-core:5.3.1' - testImplementation 'org.mockito:mockito-junit-jupiter:5.3.1' - testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.10.0' -} - -compileJava { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} - -compileTestJava { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - options.fork = true - options.forkOptions.executable = "${PATH_TO_TEST_JAVA_RUNTIME}/bin/javac" -} - -test { - useJUnitPlatform() -} - -publishing { - repositories { - maven { - url "file://$project.rootDir/repo" - } - } - publications { - mavenJava(MavenPublication) { - from components.java - artifactId = archivesBaseName - pom { - name = 'Durable Task Azure Managed Worker SDK for Java' - description = 'This package contains classes and interfaces for building Durable Task workers in Java using Azure Managed mode.' - url = "https://github.com/microsoft/durabletask-java/tree/main/azuremanaged/worker" - licenses { - license { - name = "MIT License" - url = "https://opensource.org/licenses/MIT" - distribution = "repo" - } - } - developers { - developer { - id = "Microsoft" - name = "Microsoft Corporation" - } - } - scm { - connection = "scm:git:https://github.com/microsoft/durabletask-java" - developerConnection = "scm:git:git@github.com:microsoft/durabletask-java" - url = "https://github.com/microsoft/durabletask-java/tree/main/azuremanaged/worker" - } - withXml { - project.configurations.compileOnly.allDependencies.each { dependency -> - asNode().dependencies[0].appendNode("dependency").with { - it.appendNode("groupId", dependency.group) - it.appendNode("artifactId", dependency.name) - it.appendNode("version", dependency.version) - it.appendNode("scope", "provided") - } - } - } - } - } - } -} - -java { - withSourcesJar() - withJavadocJar() -} - diff --git a/azuremanaged/settings.gradle b/azuremanaged/settings.gradle deleted file mode 100644 index 46b71477..00000000 --- a/azuremanaged/settings.gradle +++ /dev/null @@ -1,5 +0,0 @@ -rootProject.name = 'durabletask-azuremanaged' - -include 'client' -include 'worker' -include 'shared' \ No newline at end of file diff --git a/azuremanaged/shared/build.gradle b/azuremanaged/shared/build.gradle deleted file mode 100644 index 1285ab1d..00000000 --- a/azuremanaged/shared/build.gradle +++ /dev/null @@ -1,56 +0,0 @@ -plugins { - id 'java' - id 'com.google.protobuf' version '0.8.16' - id 'idea' -} - -archivesBaseName = 'durabletask-azuremanaged-shared' - -def grpcVersion = '1.59.0' -def protocVersion = '3.12.0' -def jacksonVersion = '2.15.3' -def azureCoreVersion = '1.45.0' -def azureIdentityVersion = '1.11.1' -// When build on local, you need to set this value to your local jdk11 directory. -// Java11 is used to compile and run all the tests. -def PATH_TO_TEST_JAVA_RUNTIME = System.env.JDK_11 ?: System.getProperty("java.home") - -repositories { - mavenCentral() - jcenter() -} - -dependencies { - implementation "io.grpc:grpc-protobuf:${grpcVersion}" - implementation "io.grpc:grpc-stub:${grpcVersion}" - runtimeOnly "io.grpc:grpc-netty-shaded:${grpcVersion}" - - implementation "com.azure:azure-core:${azureCoreVersion}" - implementation "com.azure:azure-identity:${azureIdentityVersion}" - - testImplementation(platform('org.junit:junit-bom:5.7.2')) - testImplementation('org.junit.jupiter:junit-jupiter') - - // Test dependencies - testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.0' - testImplementation 'org.junit.jupiter:junit-jupiter-params:5.10.0' - testImplementation 'org.mockito:mockito-core:5.3.1' - testImplementation 'org.mockito:mockito-junit-jupiter:5.3.1' - testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.10.0' -} - -compileJava { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} - -compileTestJava { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - options.fork = true - options.forkOptions.executable = "${PATH_TO_TEST_JAVA_RUNTIME}/bin/javac" -} - -test { - useJUnitPlatform() -} \ No newline at end of file diff --git a/azuremanaged/shared/src/main/java/com/microsoft/durabletask/shared/azuremanaged/AccessTokenCache.java b/azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/AccessTokenCache.java similarity index 96% rename from azuremanaged/shared/src/main/java/com/microsoft/durabletask/shared/azuremanaged/AccessTokenCache.java rename to azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/AccessTokenCache.java index 28027593..888d20b3 100644 --- a/azuremanaged/shared/src/main/java/com/microsoft/durabletask/shared/azuremanaged/AccessTokenCache.java +++ b/azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/AccessTokenCache.java @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.microsoft.durabletask.shared.azuremanaged; +package com.microsoft.durabletask.azuremanaged; import com.azure.core.credential.TokenCredential; import com.azure.core.credential.AccessToken; diff --git a/azuremanaged/dtclient/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientExtensions.java b/azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerClientExtensions.java similarity index 98% rename from azuremanaged/dtclient/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientExtensions.java rename to azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerClientExtensions.java index ae784d63..c6c62f8b 100644 --- a/azuremanaged/dtclient/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientExtensions.java +++ b/azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerClientExtensions.java @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.microsoft.durabletask.client.azuremanaged; +package com.microsoft.durabletask.azuremanaged; import com.microsoft.durabletask.DurableTaskGrpcClientBuilder; import com.azure.core.credential.TokenCredential; diff --git a/azuremanaged/dtclient/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientOptions.java b/azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerClientOptions.java similarity index 97% rename from azuremanaged/dtclient/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientOptions.java rename to azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerClientOptions.java index d2a695d6..79d024b6 100644 --- a/azuremanaged/dtclient/src/main/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientOptions.java +++ b/azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerClientOptions.java @@ -1,12 +1,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.microsoft.durabletask.client.azuremanaged; +package com.microsoft.durabletask.azuremanaged; import com.azure.core.credential.TokenCredential; import com.azure.core.credential.TokenRequestContext; -import com.microsoft.durabletask.shared.azuremanaged.DurableTaskSchedulerConnectionString; -import com.microsoft.durabletask.shared.azuremanaged.AccessTokenCache; import io.grpc.*; import javax.annotation.Nullable; import java.net.MalformedURLException; diff --git a/azuremanaged/shared/src/main/java/com/microsoft/durabletask/shared/azuremanaged/DurableTaskSchedulerConnectionString.java b/azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerConnectionString.java similarity index 98% rename from azuremanaged/shared/src/main/java/com/microsoft/durabletask/shared/azuremanaged/DurableTaskSchedulerConnectionString.java rename to azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerConnectionString.java index 0e97ca61..e98e2e96 100644 --- a/azuremanaged/shared/src/main/java/com/microsoft/durabletask/shared/azuremanaged/DurableTaskSchedulerConnectionString.java +++ b/azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerConnectionString.java @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.microsoft.durabletask.shared.azuremanaged; +package com.microsoft.durabletask.azuremanaged; import java.util.Arrays; import java.util.HashMap; diff --git a/azuremanaged/dtworker/src/main/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerExtensions.java b/azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerWorkerExtensions.java similarity index 98% rename from azuremanaged/dtworker/src/main/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerExtensions.java rename to azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerWorkerExtensions.java index 5a29c2a3..22315287 100644 --- a/azuremanaged/dtworker/src/main/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerExtensions.java +++ b/azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerWorkerExtensions.java @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.microsoft.durabletask.worker.azuremanaged; +package com.microsoft.durabletask.azuremanaged; import com.azure.core.credential.TokenCredential; import com.microsoft.durabletask.DurableTaskGrpcWorkerBuilder; diff --git a/azuremanaged/dtworker/src/main/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerOptions.java b/azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerWorkerOptions.java similarity index 97% rename from azuremanaged/dtworker/src/main/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerOptions.java rename to azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerWorkerOptions.java index 04fa7a18..ac63b81f 100644 --- a/azuremanaged/dtworker/src/main/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerOptions.java +++ b/azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerWorkerOptions.java @@ -1,12 +1,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.microsoft.durabletask.worker.azuremanaged; +package com.microsoft.durabletask.azuremanaged; import com.azure.core.credential.TokenCredential; import com.azure.core.credential.TokenRequestContext; -import com.microsoft.durabletask.shared.azuremanaged.DurableTaskSchedulerConnectionString; -import com.microsoft.durabletask.shared.azuremanaged.AccessTokenCache; import io.grpc.Channel; import io.grpc.ChannelCredentials; import io.grpc.Grpc; diff --git a/azuremanaged/shared/src/test/java/com/microsoft/durabletask/shared/azuremanaged/AccessTokenCacheTest.java b/azuremanaged/src/test/java/com/microsoft/durabletask/azuremanaged/AccessTokenCacheTest.java similarity index 98% rename from azuremanaged/shared/src/test/java/com/microsoft/durabletask/shared/azuremanaged/AccessTokenCacheTest.java rename to azuremanaged/src/test/java/com/microsoft/durabletask/azuremanaged/AccessTokenCacheTest.java index a57deee8..f900f2e9 100644 --- a/azuremanaged/shared/src/test/java/com/microsoft/durabletask/shared/azuremanaged/AccessTokenCacheTest.java +++ b/azuremanaged/src/test/java/com/microsoft/durabletask/azuremanaged/AccessTokenCacheTest.java @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.microsoft.durabletask.shared.azuremanaged; +package com.microsoft.durabletask.azuremanaged; import com.azure.core.credential.AccessToken; import com.azure.core.credential.TokenCredential; diff --git a/azuremanaged/dtclient/src/test/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientExtensionsTest.java b/azuremanaged/src/test/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerClientExtensionsTest.java similarity index 99% rename from azuremanaged/dtclient/src/test/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientExtensionsTest.java rename to azuremanaged/src/test/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerClientExtensionsTest.java index 09aef814..77c04e51 100644 --- a/azuremanaged/dtclient/src/test/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientExtensionsTest.java +++ b/azuremanaged/src/test/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerClientExtensionsTest.java @@ -1,4 +1,4 @@ -package com.microsoft.durabletask.client.azuremanaged; +package com.microsoft.durabletask.azuremanaged; import com.azure.core.credential.TokenCredential; import com.microsoft.durabletask.DurableTaskGrpcClientBuilder; diff --git a/azuremanaged/dtclient/src/test/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientOptionsTest.java b/azuremanaged/src/test/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerClientOptionsTest.java similarity index 98% rename from azuremanaged/dtclient/src/test/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientOptionsTest.java rename to azuremanaged/src/test/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerClientOptionsTest.java index 3c571e27..12102632 100644 --- a/azuremanaged/dtclient/src/test/java/com/microsoft/durabletask/client/azuremanaged/DurableTaskSchedulerClientOptionsTest.java +++ b/azuremanaged/src/test/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerClientOptionsTest.java @@ -1,7 +1,7 @@ -package com.microsoft.durabletask.client.azuremanaged; +package com.microsoft.durabletask.azuremanaged; import com.azure.core.credential.TokenCredential; -import com.microsoft.durabletask.shared.azuremanaged.DurableTaskSchedulerConnectionString; +import com.microsoft.durabletask.azuremanaged.DurableTaskSchedulerConnectionString; import io.grpc.Channel; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; diff --git a/azuremanaged/shared/src/test/java/com/microsoft/durabletask/shared/azuremanaged/DurableTaskSchedulerConnectionStringTest.java b/azuremanaged/src/test/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerConnectionStringTest.java similarity index 99% rename from azuremanaged/shared/src/test/java/com/microsoft/durabletask/shared/azuremanaged/DurableTaskSchedulerConnectionStringTest.java rename to azuremanaged/src/test/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerConnectionStringTest.java index 4047950e..bc2cff5a 100644 --- a/azuremanaged/shared/src/test/java/com/microsoft/durabletask/shared/azuremanaged/DurableTaskSchedulerConnectionStringTest.java +++ b/azuremanaged/src/test/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerConnectionStringTest.java @@ -1,4 +1,4 @@ -package com.microsoft.durabletask.shared.azuremanaged; +package com.microsoft.durabletask.azuremanaged; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.DisplayName; diff --git a/azuremanaged/dtworker/src/test/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerExtensionsTest.java b/azuremanaged/src/test/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerWorkerExtensionsTest.java similarity index 99% rename from azuremanaged/dtworker/src/test/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerExtensionsTest.java rename to azuremanaged/src/test/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerWorkerExtensionsTest.java index 04e0e2c1..b2d2f2a6 100644 --- a/azuremanaged/dtworker/src/test/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerExtensionsTest.java +++ b/azuremanaged/src/test/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerWorkerExtensionsTest.java @@ -1,4 +1,4 @@ -package com.microsoft.durabletask.worker.azuremanaged; +package com.microsoft.durabletask.azuremanaged; import com.azure.core.credential.TokenCredential; import com.microsoft.durabletask.DurableTaskGrpcWorkerBuilder; diff --git a/azuremanaged/dtworker/src/test/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerOptionsTest.java b/azuremanaged/src/test/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerWorkerOptionsTest.java similarity index 98% rename from azuremanaged/dtworker/src/test/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerOptionsTest.java rename to azuremanaged/src/test/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerWorkerOptionsTest.java index cc422923..0b94c840 100644 --- a/azuremanaged/dtworker/src/test/java/com/microsoft/durabletask/worker/azuremanaged/DurableTaskSchedulerWorkerOptionsTest.java +++ b/azuremanaged/src/test/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerWorkerOptionsTest.java @@ -1,7 +1,7 @@ -package com.microsoft.durabletask.worker.azuremanaged; +package com.microsoft.durabletask.azuremanaged; import com.azure.core.credential.TokenCredential; -import com.microsoft.durabletask.shared.azuremanaged.DurableTaskSchedulerConnectionString; +import com.microsoft.durabletask.azuremanaged.DurableTaskSchedulerConnectionString; import io.grpc.Channel; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; diff --git a/samples/build.gradle b/samples/build.gradle index 9479bfbf..e9f049e8 100644 --- a/samples/build.gradle +++ b/samples/build.gradle @@ -7,7 +7,6 @@ plugins { group 'io.durabletask' version = '0.1.0' def grpcVersion = '1.59.0' -def durabletaskClientVersion = '1.5.0' archivesBaseName = 'durabletask-samples' application { @@ -16,10 +15,8 @@ application { dependencies { implementation project(':client') - implementation project(':azuremanaged:dtclient') - implementation project(':azuremanaged:dtworker') + implementation project(':azuremanaged') - // implementation "com.microsoft:durabletask-client:${durabletaskClientVersion}" implementation 'org.springframework.boot:spring-boot-starter-web' implementation platform("org.springframework.boot:spring-boot-dependencies:2.5.2") implementation 'org.springframework.boot:spring-boot-starter' diff --git a/samples/src/main/java/io/durabletask/samples/WebAppToDurableTaskSchedulerSample.java b/samples/src/main/java/io/durabletask/samples/WebAppToDurableTaskSchedulerSample.java index 8cb887c6..a5236713 100644 --- a/samples/src/main/java/io/durabletask/samples/WebAppToDurableTaskSchedulerSample.java +++ b/samples/src/main/java/io/durabletask/samples/WebAppToDurableTaskSchedulerSample.java @@ -4,8 +4,8 @@ import com.azure.core.credential.TokenCredential; import com.microsoft.durabletask.*; -import com.microsoft.durabletask.client.azuremanaged.DurableTaskSchedulerClientExtensions; -import com.microsoft.durabletask.worker.azuremanaged.DurableTaskSchedulerWorkerExtensions; +import com.microsoft.durabletask.azuremanaged.DurableTaskSchedulerClientExtensions; +import com.microsoft.durabletask.azuremanaged.DurableTaskSchedulerWorkerExtensions; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.*; diff --git a/settings.gradle b/settings.gradle index ace62edc..3813e3ae 100644 --- a/settings.gradle +++ b/settings.gradle @@ -3,9 +3,6 @@ rootProject.name = 'durabletask-java' include ":client" include ":azurefunctions" include ":azuremanaged" -include ":azuremanaged:dtclient" -include ":azuremanaged:dtworker" -include ":azuremanaged:shared" include ":samples" include ":samples-azure-functions" include ":endtoendtests" From 1bebf5a0c2d286ae98448ec3af7acfcc5436c9a6 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sun, 23 Mar 2025 11:12:38 -0700 Subject: [PATCH 50/53] conn str support --- azuremanaged/build.gradle | 2 +- .../DurableTaskSchedulerClientExtensions.java | 12 +- .../DurableTaskSchedulerClientOptions.java | 22 +-- .../DurableTaskSchedulerConnectionString.java | 66 ++++++- .../DurableTaskSchedulerWorkerExtensions.java | 12 +- .../DurableTaskSchedulerWorkerOptions.java | 23 +-- ...ableTaskSchedulerClientExtensionsTest.java | 10 +- ...DurableTaskSchedulerClientOptionsTest.java | 170 +++++------------- ...ableTaskSchedulerConnectionStringTest.java | 55 ++++++ ...ableTaskSchedulerWorkerExtensionsTest.java | 11 +- ...DurableTaskSchedulerWorkerOptionsTest.java | 51 +----- .../WebAppToDurableTaskSchedulerSample.java | 28 +-- .../src/main/resources/application.properties | 9 +- ...-app-to-durable-task-scheduler-sample.http | 2 +- 14 files changed, 211 insertions(+), 262 deletions(-) diff --git a/azuremanaged/build.gradle b/azuremanaged/build.gradle index 97e09a6b..c5d645b6 100644 --- a/azuremanaged/build.gradle +++ b/azuremanaged/build.gradle @@ -16,7 +16,7 @@ plugins { archivesBaseName = 'durabletask-azuremanaged' group 'com.microsoft' -version = '1.5.0' +version = '1.5.0-preview.1' def grpcVersion = '1.59.0' def azureCoreVersion = '1.45.0' diff --git a/azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerClientExtensions.java b/azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerClientExtensions.java index c6c62f8b..ffc7d446 100644 --- a/azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerClientExtensions.java +++ b/azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerClientExtensions.java @@ -22,18 +22,16 @@ private DurableTaskSchedulerClientExtensions() {} * * @param builder The builder to configure. * @param connectionString The connection string for Azure-managed Durable Task Scheduler. - * @param tokenCredential The token credential for authentication, or null to use connection string credentials. * @throws NullPointerException if builder or connectionString is null */ public static void useDurableTaskScheduler( DurableTaskGrpcClientBuilder builder, - String connectionString, - @Nullable TokenCredential tokenCredential) { + String connectionString) { Objects.requireNonNull(builder, "builder must not be null"); Objects.requireNonNull(connectionString, "connectionString must not be null"); configureBuilder(builder, - DurableTaskSchedulerClientOptions.fromConnectionString(connectionString, tokenCredential)); + DurableTaskSchedulerClientOptions.fromConnectionString(connectionString)); } /** @@ -64,16 +62,14 @@ public static void useDurableTaskScheduler( * Creates a DurableTaskGrpcClientBuilder configured for Azure-managed Durable Task Scheduler using a connection string. * * @param connectionString The connection string for Azure-managed Durable Task Scheduler. - * @param tokenCredential The token credential for authentication, or null to use connection string credentials. * @return A new configured DurableTaskGrpcClientBuilder instance. * @throws NullPointerException if connectionString is null */ public static DurableTaskGrpcClientBuilder createClientBuilder( - String connectionString, - @Nullable TokenCredential tokenCredential) { + String connectionString) { Objects.requireNonNull(connectionString, "connectionString must not be null"); return createBuilderFromOptions( - DurableTaskSchedulerClientOptions.fromConnectionString(connectionString, tokenCredential)); + DurableTaskSchedulerClientOptions.fromConnectionString(connectionString)); } /** diff --git a/azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerClientOptions.java b/azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerClientOptions.java index 79d024b6..e2bca74c 100644 --- a/azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerClientOptions.java +++ b/azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerClientOptions.java @@ -6,7 +6,6 @@ import com.azure.core.credential.TokenCredential; import com.azure.core.credential.TokenRequestContext; import io.grpc.*; -import javax.annotation.Nullable; import java.net.MalformedURLException; import java.time.Duration; import java.util.Objects; @@ -34,12 +33,11 @@ public DurableTaskSchedulerClientOptions() { * Creates a new instance of DurableTaskSchedulerClientOptions from a connection string. * * @param connectionString The connection string to parse. - * @param credential The credential to use for authentication. It is nullable for anonymous access. * @return A new DurableTaskSchedulerClientOptions object. */ - public static DurableTaskSchedulerClientOptions fromConnectionString(String connectionString, @Nullable TokenCredential credential) { + public static DurableTaskSchedulerClientOptions fromConnectionString(String connectionString) { DurableTaskSchedulerConnectionString parsedConnectionString = new DurableTaskSchedulerConnectionString(connectionString); - return fromConnectionString(parsedConnectionString, credential); + return fromConnectionString(parsedConnectionString); } /** @@ -48,13 +46,13 @@ public static DurableTaskSchedulerClientOptions fromConnectionString(String conn * @param connectionString The parsed connection string. * @return A new DurableTaskSchedulerClientOptions object. */ - static DurableTaskSchedulerClientOptions fromConnectionString(DurableTaskSchedulerConnectionString connectionString, @Nullable TokenCredential credential) { + static DurableTaskSchedulerClientOptions fromConnectionString(DurableTaskSchedulerConnectionString connectionString) { // TODO: Parse different credential types from connection string DurableTaskSchedulerClientOptions options = new DurableTaskSchedulerClientOptions(); options.setEndpointAddress(connectionString.getEndpoint()); options.setTaskHubName(connectionString.getTaskHubName()); - options.setCredential(credential); - options.setAllowInsecureCredentials(credential == null); + options.setCredential(connectionString.getCredential()); + options.setAllowInsecureCredentials(options.getCredential() == null); return options; } @@ -178,16 +176,6 @@ public DurableTaskSchedulerClientOptions setTokenRefreshMargin(Duration tokenRef return this; } - /** - * Validates that the options are properly configured. - * - * @throws IllegalArgumentException If the options are not properly configured. - */ - public void validate() { - Objects.requireNonNull(endpointAddress, "endpointAddress must not be null"); - Objects.requireNonNull(taskHubName, "taskHubName must not be null"); - } - /** * Creates a gRPC channel using the configured options. * diff --git a/azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerConnectionString.java b/azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerConnectionString.java index e98e2e96..01426f8e 100644 --- a/azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerConnectionString.java +++ b/azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerConnectionString.java @@ -8,6 +8,16 @@ import java.util.List; import java.util.Map; +import javax.annotation.Nullable; + +import com.azure.core.credential.TokenCredential; +import com.azure.identity.AzureCliCredentialBuilder; +import com.azure.identity.AzurePowerShellCredentialBuilder; +import com.azure.identity.DefaultAzureCredentialBuilder; +import com.azure.identity.EnvironmentCredentialBuilder; +import com.azure.identity.ManagedIdentityCredentialBuilder; +import com.azure.identity.WorkloadIdentityCredentialBuilder; + /** * Represents the constituent parts of a connection string for a Durable Task Scheduler service. */ @@ -27,9 +37,9 @@ public DurableTaskSchedulerConnectionString(String connectionString) { this.properties = parseConnectionString(connectionString); // Validate required properties - getRequiredValue("Endpoint"); - getRequiredValue("Authentication"); - getRequiredValue("TaskHub"); + this.getAuthentication(); + this.getTaskHubName(); + this.getEndpoint(); } /** @@ -127,4 +137,54 @@ private static Map parseConnectionString(String connectionString return properties; } + + /** + * Gets a TokenCredential based on the authentication type specified in the connection string. + * + * @return A TokenCredential instance based on the specified authentication type, or null if authentication type is "none". + * @throws IllegalArgumentException If the connection string contains an unsupported authentication type. + */ + public @Nullable TokenCredential getCredential() { + String authType = getAuthentication(); + + // Parse the supported auth types in a case-insensitive way + switch (authType.toLowerCase().trim()) { + case "defaultazure": + return new DefaultAzureCredentialBuilder().build(); + case "managedidentity": + return new ManagedIdentityCredentialBuilder().clientId(getClientId()).build(); + case "workloadidentity": + WorkloadIdentityCredentialBuilder builder = new WorkloadIdentityCredentialBuilder(); + if (getClientId() != null && !getClientId().isEmpty()) { + builder.clientId(getClientId()); + } + + if (getTenantId() != null && !getTenantId().isEmpty()) { + builder.tenantId(getTenantId()); + } + + if (getTokenFilePath() != null && !getTokenFilePath().isEmpty()) { + builder.tokenFilePath(getTokenFilePath()); + } + + if (getAdditionallyAllowedTenants() != null) { + for (String tenant : getAdditionallyAllowedTenants()) { + builder.additionallyAllowedTenants(tenant); + } + } + + return builder.build(); + case "environment": + return new EnvironmentCredentialBuilder().build(); + case "azurecli": + return new AzureCliCredentialBuilder().build(); + case "azurepowershell": + return new AzurePowerShellCredentialBuilder().build(); + case "none": + return null; + default: + throw new IllegalArgumentException( + String.format("The connection string contains an unsupported authentication type '%s'.", authType)); + } + } } \ No newline at end of file diff --git a/azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerWorkerExtensions.java b/azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerWorkerExtensions.java index 22315287..0ea9b5be 100644 --- a/azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerWorkerExtensions.java +++ b/azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerWorkerExtensions.java @@ -22,18 +22,16 @@ private DurableTaskSchedulerWorkerExtensions() {} * * @param builder The builder to configure. * @param connectionString The connection string for Azure-managed Durable Task Scheduler. - * @param tokenCredential The token credential for authentication, or null to use connection string credentials. * @throws NullPointerException if builder or connectionString is null */ public static void useDurableTaskScheduler( DurableTaskGrpcWorkerBuilder builder, - String connectionString, - @Nullable TokenCredential tokenCredential) { + String connectionString) { Objects.requireNonNull(builder, "builder must not be null"); Objects.requireNonNull(connectionString, "connectionString must not be null"); configureBuilder(builder, - DurableTaskSchedulerWorkerOptions.fromConnectionString(connectionString, tokenCredential)); + DurableTaskSchedulerWorkerOptions.fromConnectionString(connectionString)); } /** @@ -64,16 +62,14 @@ public static void useDurableTaskScheduler( * Creates a DurableTaskGrpcWorkerBuilder configured for Azure-managed Durable Task Scheduler using a connection string. * * @param connectionString The connection string for Azure-managed Durable Task Scheduler. - * @param tokenCredential The token credential for authentication, or null to use connection string credentials. * @return A new configured DurableTaskGrpcWorkerBuilder instance. * @throws NullPointerException if connectionString is null */ public static DurableTaskGrpcWorkerBuilder createWorkerBuilder( - String connectionString, - @Nullable TokenCredential tokenCredential) { + String connectionString) { Objects.requireNonNull(connectionString, "connectionString must not be null"); return createBuilderFromOptions( - DurableTaskSchedulerWorkerOptions.fromConnectionString(connectionString, tokenCredential)); + DurableTaskSchedulerWorkerOptions.fromConnectionString(connectionString)); } /** diff --git a/azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerWorkerOptions.java b/azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerWorkerOptions.java index ac63b81f..03f9d60a 100644 --- a/azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerWorkerOptions.java +++ b/azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerWorkerOptions.java @@ -21,7 +21,6 @@ import java.util.Objects; import java.net.URL; import java.net.MalformedURLException; -import javax.annotation.Nullable; /** * Options for configuring the Durable Task Scheduler worker. @@ -45,27 +44,25 @@ public DurableTaskSchedulerWorkerOptions() { * Creates a new instance of DurableTaskSchedulerWorkerOptions from a connection string. * * @param connectionString The connection string to parse. - * @param credential The token credential for authentication, or null to use connection string credentials. * @return A new DurableTaskSchedulerWorkerOptions object. */ - public static DurableTaskSchedulerWorkerOptions fromConnectionString(String connectionString, @Nullable TokenCredential credential) { + public static DurableTaskSchedulerWorkerOptions fromConnectionString(String connectionString) { DurableTaskSchedulerConnectionString parsedConnectionString = new DurableTaskSchedulerConnectionString(connectionString); - return fromConnectionString(parsedConnectionString, credential); + return fromConnectionString(parsedConnectionString); } /** * Creates a new instance of DurableTaskSchedulerWorkerOptions from a parsed connection string. * * @param connectionString The parsed connection string. - * @param credential The token credential for authentication, or null to use connection string credentials. * @return A new DurableTaskSchedulerWorkerOptions object. */ - static DurableTaskSchedulerWorkerOptions fromConnectionString(DurableTaskSchedulerConnectionString connectionString, @Nullable TokenCredential credential) { + static DurableTaskSchedulerWorkerOptions fromConnectionString(DurableTaskSchedulerConnectionString connectionString) { DurableTaskSchedulerWorkerOptions options = new DurableTaskSchedulerWorkerOptions(); options.setEndpointAddress(connectionString.getEndpoint()); options.setTaskHubName(connectionString.getTaskHubName()); - options.setCredential(credential); - options.setAllowInsecureCredentials(credential == null); + options.setCredential(connectionString.getCredential()); + options.setAllowInsecureCredentials(options.getCredential() == null); return options; } @@ -189,16 +186,6 @@ public DurableTaskSchedulerWorkerOptions setTokenRefreshMargin(Duration tokenRef return this; } - /** - * Validates that the options are properly configured. - * - * @throws IllegalArgumentException If the options are not properly configured. - */ - public void validate() { - Objects.requireNonNull(endpointAddress, "endpointAddress must not be null"); - Objects.requireNonNull(taskHubName, "taskHubName must not be null"); - } - /** * Creates a gRPC channel using the configured options. * diff --git a/azuremanaged/src/test/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerClientExtensionsTest.java b/azuremanaged/src/test/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerClientExtensionsTest.java index 77c04e51..a486dd4d 100644 --- a/azuremanaged/src/test/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerClientExtensionsTest.java +++ b/azuremanaged/src/test/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerClientExtensionsTest.java @@ -37,7 +37,7 @@ public void useDurableTaskScheduler_WithConnectionString_ConfiguresBuilder() { // Act DurableTaskSchedulerClientExtensions.useDurableTaskScheduler( - mockBuilder, VALID_CONNECTION_STRING, mockCredential); + mockBuilder, VALID_CONNECTION_STRING); // Assert verify(mockBuilder).grpcChannel(any(Channel.class)); @@ -49,7 +49,7 @@ public void useDurableTaskScheduler_WithConnectionString_ThrowsForNullBuilder() // Act & Assert assertThrows(NullPointerException.class, () -> DurableTaskSchedulerClientExtensions.useDurableTaskScheduler( - null, VALID_CONNECTION_STRING, mockCredential)); + null, VALID_CONNECTION_STRING)); } @Test @@ -58,7 +58,7 @@ public void useDurableTaskScheduler_WithConnectionString_ThrowsForNullConnection // Act & Assert assertThrows(NullPointerException.class, () -> DurableTaskSchedulerClientExtensions.useDurableTaskScheduler( - mockBuilder, null, mockCredential)); + mockBuilder, null)); } @Test @@ -107,7 +107,7 @@ public void useDurableTaskScheduler_WithExplicitParameters_ThrowsForNullTaskHubN public void createClientBuilder_WithConnectionString_CreatesValidBuilder() { // Act DurableTaskGrpcClientBuilder result = - DurableTaskSchedulerClientExtensions.createClientBuilder(VALID_CONNECTION_STRING, mockCredential); + DurableTaskSchedulerClientExtensions.createClientBuilder(VALID_CONNECTION_STRING); // Assert assertNotNull(result); @@ -118,7 +118,7 @@ public void createClientBuilder_WithConnectionString_CreatesValidBuilder() { public void createClientBuilder_WithConnectionString_ThrowsForNullConnectionString() { // Act & Assert assertThrows(NullPointerException.class, - () -> DurableTaskSchedulerClientExtensions.createClientBuilder(null, mockCredential)); + () -> DurableTaskSchedulerClientExtensions.createClientBuilder(null)); } @Test diff --git a/azuremanaged/src/test/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerClientOptionsTest.java b/azuremanaged/src/test/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerClientOptionsTest.java index 12102632..8508b56b 100644 --- a/azuremanaged/src/test/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerClientOptionsTest.java +++ b/azuremanaged/src/test/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerClientOptionsTest.java @@ -1,7 +1,6 @@ package com.microsoft.durabletask.azuremanaged; import com.azure.core.credential.TokenCredential; -import com.microsoft.durabletask.azuremanaged.DurableTaskSchedulerConnectionString; import io.grpc.Channel; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -19,67 +18,40 @@ @ExtendWith(MockitoExtension.class) public class DurableTaskSchedulerClientOptionsTest { - private static final String VALID_ENDPOINT = "https://example.com"; - private static final String VALID_TASKHUB = "myTaskHub"; private static final String VALID_CONNECTION_STRING = "Endpoint=https://example.com;Authentication=ManagedIdentity;TaskHub=myTaskHub"; + private static final String VALID_ENDPOINT = "https://example.com"; + private static final String VALID_TASKHUB = "myTaskHub"; + private static final String CUSTOM_RESOURCE_ID = "https://custom.resource"; + private static final Duration CUSTOM_REFRESH_MARGIN = Duration.ofMinutes(10); @Mock private TokenCredential mockCredential; @Test - @DisplayName("Default constructor should set default values") - public void defaultConstructor_SetsDefaultValues() { - // Act - DurableTaskSchedulerClientOptions options = new DurableTaskSchedulerClientOptions(); - - // Assert - assertEquals("", options.getEndpointAddress()); - assertEquals("", options.getTaskHubName()); - assertNull(options.getCredential()); - assertEquals("https://durabletask.io", options.getResourceId()); - assertFalse(options.isAllowInsecureCredentials()); - assertEquals(Duration.ofMinutes(5), options.getTokenRefreshMargin()); - } - - @Test - @DisplayName("fromConnectionString should parse connection string correctly") - public void fromConnectionString_ParsesConnectionStringCorrectly() { + @DisplayName("fromConnectionString should create valid options") + public void fromConnectionString_CreatesValidOptions() { // Act DurableTaskSchedulerClientOptions options = - DurableTaskSchedulerClientOptions.fromConnectionString(VALID_CONNECTION_STRING, mockCredential); - + DurableTaskSchedulerClientOptions.fromConnectionString(VALID_CONNECTION_STRING); + // Assert + assertNotNull(options); assertEquals(VALID_ENDPOINT, options.getEndpointAddress()); assertEquals(VALID_TASKHUB, options.getTaskHubName()); - assertSame(mockCredential, options.getCredential()); - assertFalse(options.isAllowInsecureCredentials()); - } - - @Test - @DisplayName("fromConnectionString should allow insecure credentials when credential is null") - public void fromConnectionString_AllowsInsecureCredentialsWhenCredentialIsNull() { - // Act - DurableTaskSchedulerClientOptions options = - DurableTaskSchedulerClientOptions.fromConnectionString(VALID_CONNECTION_STRING, null); - - // Assert - assertTrue(options.isAllowInsecureCredentials()); - assertNull(options.getCredential()); } @Test - @DisplayName("setEndpointAddress should update endpoint address") - public void setEndpointAddress_UpdatesEndpointAddress() { + @DisplayName("setEndpointAddress should update endpoint") + public void setEndpointAddress_UpdatesEndpoint() { // Arrange DurableTaskSchedulerClientOptions options = new DurableTaskSchedulerClientOptions(); - + // Act - DurableTaskSchedulerClientOptions result = options.setEndpointAddress(VALID_ENDPOINT); - + options.setEndpointAddress(VALID_ENDPOINT); + // Assert assertEquals(VALID_ENDPOINT, options.getEndpointAddress()); - assertSame(options, result); // Builder pattern returns this } @Test @@ -87,13 +59,12 @@ public void setEndpointAddress_UpdatesEndpointAddress() { public void setTaskHubName_UpdatesTaskHubName() { // Arrange DurableTaskSchedulerClientOptions options = new DurableTaskSchedulerClientOptions(); - + // Act - DurableTaskSchedulerClientOptions result = options.setTaskHubName(VALID_TASKHUB); - + options.setTaskHubName(VALID_TASKHUB); + // Assert assertEquals(VALID_TASKHUB, options.getTaskHubName()); - assertSame(options, result); // Builder pattern returns this } @Test @@ -101,13 +72,12 @@ public void setTaskHubName_UpdatesTaskHubName() { public void setCredential_UpdatesCredential() { // Arrange DurableTaskSchedulerClientOptions options = new DurableTaskSchedulerClientOptions(); - + // Act - DurableTaskSchedulerClientOptions result = options.setCredential(mockCredential); - + options.setCredential(mockCredential); + // Assert - assertSame(mockCredential, options.getCredential()); - assertSame(options, result); // Builder pattern returns this + assertEquals(mockCredential, options.getCredential()); } @Test @@ -115,117 +85,67 @@ public void setCredential_UpdatesCredential() { public void setResourceId_UpdatesResourceId() { // Arrange DurableTaskSchedulerClientOptions options = new DurableTaskSchedulerClientOptions(); - String customResourceId = "https://custom-resource.example.com"; - + // Act - DurableTaskSchedulerClientOptions result = options.setResourceId(customResourceId); - + options.setResourceId(CUSTOM_RESOURCE_ID); + // Assert - assertEquals(customResourceId, options.getResourceId()); - assertSame(options, result); // Builder pattern returns this + assertEquals(CUSTOM_RESOURCE_ID, options.getResourceId()); } @Test - @DisplayName("setAllowInsecureCredentials should update allowInsecureCredentials") - public void setAllowInsecureCredentials_UpdatesAllowInsecureCredentials() { + @DisplayName("setAllowInsecureCredentials should update insecure credentials flag") + public void setAllowInsecureCredentials_UpdatesFlag() { // Arrange DurableTaskSchedulerClientOptions options = new DurableTaskSchedulerClientOptions(); - + // Act - DurableTaskSchedulerClientOptions result = options.setAllowInsecureCredentials(true); - + options.setAllowInsecureCredentials(true); + // Assert assertTrue(options.isAllowInsecureCredentials()); - assertSame(options, result); // Builder pattern returns this } @Test @DisplayName("setTokenRefreshMargin should update token refresh margin") - public void setTokenRefreshMargin_UpdatesTokenRefreshMargin() { + public void setTokenRefreshMargin_UpdatesMargin() { // Arrange DurableTaskSchedulerClientOptions options = new DurableTaskSchedulerClientOptions(); - Duration customMargin = Duration.ofMinutes(10); - + // Act - DurableTaskSchedulerClientOptions result = options.setTokenRefreshMargin(customMargin); - + options.setTokenRefreshMargin(CUSTOM_REFRESH_MARGIN); + // Assert - assertEquals(customMargin, options.getTokenRefreshMargin()); - assertSame(options, result); // Builder pattern returns this + assertEquals(CUSTOM_REFRESH_MARGIN, options.getTokenRefreshMargin()); } @Test - @DisplayName("validate should not throw when options are valid") - public void validate_DoesNotThrowWhenOptionsAreValid() { + @DisplayName("createGrpcChannel should create valid channel") + public void createGrpcChannel_CreatesValidChannel() { // Arrange DurableTaskSchedulerClientOptions options = new DurableTaskSchedulerClientOptions() .setEndpointAddress(VALID_ENDPOINT) .setTaskHubName(VALID_TASKHUB); - - // Act & Assert - assertDoesNotThrow(() -> options.validate()); - } - - @Test - @DisplayName("validate should throw when endpoint address is null") - public void validate_ThrowsWhenEndpointAddressIsNull() { - // Arrange - DurableTaskSchedulerClientOptions options = new DurableTaskSchedulerClientOptions() - .setEndpointAddress(null) - .setTaskHubName(VALID_TASKHUB); - - // Act & Assert - assertThrows(NullPointerException.class, () -> options.validate()); - } - @Test - @DisplayName("validate should throw when task hub name is null") - public void validate_ThrowsWhenTaskHubNameIsNull() { - // Arrange - DurableTaskSchedulerClientOptions options = new DurableTaskSchedulerClientOptions() - .setEndpointAddress(VALID_ENDPOINT) - .setTaskHubName(null); - - // Act & Assert - assertThrows(NullPointerException.class, () -> options.validate()); - } - - @Test - @DisplayName("createGrpcChannel should create a channel") - public void createGrpcChannel_CreatesChannel() { - // Arrange - DurableTaskSchedulerClientOptions options = new DurableTaskSchedulerClientOptions() - .setEndpointAddress(VALID_ENDPOINT) - .setTaskHubName(VALID_TASKHUB); - // Act Channel channel = options.createGrpcChannel(); - + // Assert assertNotNull(channel); } @Test - @DisplayName("createGrpcChannel should handle endpoints without protocol") - public void createGrpcChannel_HandlesEndpointsWithoutProtocol() { + @DisplayName("createGrpcChannel should handle endpoint without protocol") + public void createGrpcChannel_HandlesEndpointWithoutProtocol() { // Arrange DurableTaskSchedulerClientOptions options = new DurableTaskSchedulerClientOptions() .setEndpointAddress("example.com") .setTaskHubName(VALID_TASKHUB); - - // Act & Assert - assertDoesNotThrow(() -> options.createGrpcChannel()); - } - @Test - @DisplayName("createGrpcChannel should throw for invalid URLs") - public void createGrpcChannel_ThrowsForInvalidUrls() { - // Arrange - DurableTaskSchedulerClientOptions options = new DurableTaskSchedulerClientOptions() - .setEndpointAddress("invalid:url") - .setTaskHubName(VALID_TASKHUB); - - // Act & Assert - assertThrows(IllegalArgumentException.class, () -> options.createGrpcChannel()); + // Act + Channel channel = options.createGrpcChannel(); + + // Assert + assertNotNull(channel); } } \ No newline at end of file diff --git a/azuremanaged/src/test/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerConnectionStringTest.java b/azuremanaged/src/test/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerConnectionStringTest.java index bc2cff5a..cf8dcf13 100644 --- a/azuremanaged/src/test/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerConnectionStringTest.java +++ b/azuremanaged/src/test/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerConnectionStringTest.java @@ -1,5 +1,7 @@ package com.microsoft.durabletask.azuremanaged; +import com.azure.core.credential.TokenCredential; +import com.azure.identity.*; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.params.ParameterizedTest; @@ -169,4 +171,57 @@ public void getTokenFilePath_ReturnsValueWhenPresent() { // Assert assertEquals("/path/to/token", parsedString.getTokenFilePath()); } + + @Test + @DisplayName("getCredential should handle supported authentication types") + public void getCredential_HandlesSupportedAuthTypes() { + // Arrange + String connectionString = String.format( + "Endpoint=%s;Authentication=%s;TaskHub=%s", + "https://example.com", "ManagedIdentity", "myTaskHub"); + + // Act + DurableTaskSchedulerConnectionString result = + new DurableTaskSchedulerConnectionString(connectionString); + + // Assert + TokenCredential credential = result.getCredential(); + assertNotNull(credential); + + // Verify the correct credential type is returned + assertTrue(credential instanceof ManagedIdentityCredential); + } + + @Test + @DisplayName("getCredential should throw for unsupported authentication type") + public void getCredential_ThrowsForUnsupportedAuthType() { + // Arrange + String connectionString = String.format( + "Endpoint=%s;Authentication=%s;TaskHub=%s", + "https://example.com", "UnsupportedType", "myTaskHub"); + DurableTaskSchedulerConnectionString result = + new DurableTaskSchedulerConnectionString(connectionString); + + // Act & Assert + assertThrows(IllegalArgumentException.class, result::getCredential); + } + + @Test + @DisplayName("getCredential should configure WorkloadIdentity with all properties") + public void getCredential_ConfiguresWorkloadIdentityWithAllProperties() { + // Arrange + String connectionString = String.format( + "Endpoint=%s;Authentication=%s;TaskHub=%s;ClientID=%s;TenantId=%s;TokenFilePath=%s;AdditionallyAllowedTenants=%s", + "https://example.com", "WorkloadIdentity", "myTaskHub", "client-id-123", "tenant-id-123", + "/path/to/token", "tenant1,tenant2,tenant3"); + + // Act + DurableTaskSchedulerConnectionString result = + new DurableTaskSchedulerConnectionString(connectionString); + TokenCredential credential = result.getCredential(); + + // Assert + assertNotNull(credential); + assertTrue(credential instanceof WorkloadIdentityCredential); + } } \ No newline at end of file diff --git a/azuremanaged/src/test/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerWorkerExtensionsTest.java b/azuremanaged/src/test/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerWorkerExtensionsTest.java index b2d2f2a6..9ad18281 100644 --- a/azuremanaged/src/test/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerWorkerExtensionsTest.java +++ b/azuremanaged/src/test/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerWorkerExtensionsTest.java @@ -8,7 +8,6 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.mockito.ArgumentCaptor; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; @@ -38,7 +37,7 @@ public void useDurableTaskScheduler_WithConnectionString_ConfiguresBuilder() { // Act DurableTaskSchedulerWorkerExtensions.useDurableTaskScheduler( - mockBuilder, VALID_CONNECTION_STRING, mockCredential); + mockBuilder, VALID_CONNECTION_STRING); // Assert verify(mockBuilder).grpcChannel(any(Channel.class)); @@ -50,7 +49,7 @@ public void useDurableTaskScheduler_WithConnectionString_ThrowsForNullBuilder() // Act & Assert assertThrows(NullPointerException.class, () -> DurableTaskSchedulerWorkerExtensions.useDurableTaskScheduler( - null, VALID_CONNECTION_STRING, mockCredential)); + null, VALID_CONNECTION_STRING)); } @Test @@ -59,7 +58,7 @@ public void useDurableTaskScheduler_WithConnectionString_ThrowsForNullConnection // Act & Assert assertThrows(NullPointerException.class, () -> DurableTaskSchedulerWorkerExtensions.useDurableTaskScheduler( - mockBuilder, null, mockCredential)); + mockBuilder, null)); } @Test @@ -108,7 +107,7 @@ public void useDurableTaskScheduler_WithExplicitParameters_ThrowsForNullTaskHubN public void createWorkerBuilder_WithConnectionString_CreatesValidBuilder() { // Act DurableTaskGrpcWorkerBuilder result = - DurableTaskSchedulerWorkerExtensions.createWorkerBuilder(VALID_CONNECTION_STRING, mockCredential); + DurableTaskSchedulerWorkerExtensions.createWorkerBuilder(VALID_CONNECTION_STRING); // Assert assertNotNull(result); @@ -119,7 +118,7 @@ public void createWorkerBuilder_WithConnectionString_CreatesValidBuilder() { public void createWorkerBuilder_WithConnectionString_ThrowsForNullConnectionString() { // Act & Assert assertThrows(NullPointerException.class, - () -> DurableTaskSchedulerWorkerExtensions.createWorkerBuilder(null, mockCredential)); + () -> DurableTaskSchedulerWorkerExtensions.createWorkerBuilder(null)); } @Test diff --git a/azuremanaged/src/test/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerWorkerOptionsTest.java b/azuremanaged/src/test/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerWorkerOptionsTest.java index 0b94c840..5fab6912 100644 --- a/azuremanaged/src/test/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerWorkerOptionsTest.java +++ b/azuremanaged/src/test/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerWorkerOptionsTest.java @@ -1,7 +1,6 @@ package com.microsoft.durabletask.azuremanaged; import com.azure.core.credential.TokenCredential; -import com.microsoft.durabletask.azuremanaged.DurableTaskSchedulerConnectionString; import io.grpc.Channel; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -47,21 +46,25 @@ public void defaultConstructor_SetsDefaultValues() { public void fromConnectionString_ParsesConnectionStringCorrectly() { // Act DurableTaskSchedulerWorkerOptions options = - DurableTaskSchedulerWorkerOptions.fromConnectionString(VALID_CONNECTION_STRING, mockCredential); + DurableTaskSchedulerWorkerOptions.fromConnectionString(VALID_CONNECTION_STRING); // Assert assertEquals(VALID_ENDPOINT, options.getEndpointAddress()); assertEquals(VALID_TASKHUB, options.getTaskHubName()); - assertSame(mockCredential, options.getCredential()); + assertNotNull(options.getCredential()); + assertTrue(options.getCredential() instanceof com.azure.identity.ManagedIdentityCredential); assertFalse(options.isAllowInsecureCredentials()); } @Test - @DisplayName("fromConnectionString should allow insecure credentials when credential is null") - public void fromConnectionString_AllowsInsecureCredentialsWhenCredentialIsNull() { + @DisplayName("fromConnectionString should allow insecure credentials when authentication is None") + public void fromConnectionString_AllowsInsecureCredentialsWhenAuthenticationIsNone() { + // Arrange + String connectionString = "Endpoint=https://example.com;Authentication=None;TaskHub=myTaskHub"; + // Act DurableTaskSchedulerWorkerOptions options = - DurableTaskSchedulerWorkerOptions.fromConnectionString(VALID_CONNECTION_STRING, null); + DurableTaskSchedulerWorkerOptions.fromConnectionString(connectionString); // Assert assertTrue(options.isAllowInsecureCredentials()); @@ -154,42 +157,6 @@ public void setTokenRefreshMargin_UpdatesTokenRefreshMargin() { assertSame(options, result); // Builder pattern returns this } - @Test - @DisplayName("validate should not throw when options are valid") - public void validate_DoesNotThrowWhenOptionsAreValid() { - // Arrange - DurableTaskSchedulerWorkerOptions options = new DurableTaskSchedulerWorkerOptions() - .setEndpointAddress(VALID_ENDPOINT) - .setTaskHubName(VALID_TASKHUB); - - // Act & Assert - assertDoesNotThrow(() -> options.validate()); - } - - @Test - @DisplayName("validate should throw when endpoint address is null") - public void validate_ThrowsWhenEndpointAddressIsNull() { - // Arrange - DurableTaskSchedulerWorkerOptions options = new DurableTaskSchedulerWorkerOptions() - .setEndpointAddress(null) - .setTaskHubName(VALID_TASKHUB); - - // Act & Assert - assertThrows(NullPointerException.class, () -> options.validate()); - } - - @Test - @DisplayName("validate should throw when task hub name is null") - public void validate_ThrowsWhenTaskHubNameIsNull() { - // Arrange - DurableTaskSchedulerWorkerOptions options = new DurableTaskSchedulerWorkerOptions() - .setEndpointAddress(VALID_ENDPOINT) - .setTaskHubName(null); - - // Act & Assert - assertThrows(NullPointerException.class, () -> options.validate()); - } - @Test @DisplayName("createGrpcChannel should create a channel") public void createGrpcChannel_CreatesChannel() { diff --git a/samples/src/main/java/io/durabletask/samples/WebAppToDurableTaskSchedulerSample.java b/samples/src/main/java/io/durabletask/samples/WebAppToDurableTaskSchedulerSample.java index a5236713..d09fdd69 100644 --- a/samples/src/main/java/io/durabletask/samples/WebAppToDurableTaskSchedulerSample.java +++ b/samples/src/main/java/io/durabletask/samples/WebAppToDurableTaskSchedulerSample.java @@ -2,7 +2,6 @@ // Licensed under the MIT License. package io.durabletask.samples; -import com.azure.core.credential.TokenCredential; import com.microsoft.durabletask.*; import com.microsoft.durabletask.azuremanaged.DurableTaskSchedulerClientExtensions; import com.microsoft.durabletask.azuremanaged.DurableTaskSchedulerWorkerExtensions; @@ -14,8 +13,6 @@ import org.springframework.context.annotation.Configuration; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; -import java.time.Duration; -import com.azure.identity.DefaultAzureCredentialBuilder; @ConfigurationProperties(prefix = "durable.task") @lombok.Data @@ -23,7 +20,7 @@ class DurableTaskProperties { private String endpoint; private String taskHubName; private String resourceId = "https://durabletask.io"; - private boolean allowInsecureCredentials = false; + private String connectionString; } /** @@ -47,22 +44,13 @@ public static void main(String[] args) { @Configuration static class DurableTaskConfig { - - @Bean - public TokenCredential tokenCredential() { - return new DefaultAzureCredentialBuilder().build(); - } - @Bean public DurableTaskGrpcWorker durableTaskWorker( - DurableTaskProperties properties, - TokenCredential tokenCredential) { + DurableTaskProperties properties) { // Create worker using Azure-managed extensions DurableTaskGrpcWorkerBuilder workerBuilder = DurableTaskSchedulerWorkerExtensions.createWorkerBuilder( - properties.getEndpoint(), - properties.getTaskHubName(), - tokenCredential); + properties.getConnectionString()); // Add orchestrations using the factory pattern workerBuilder.addOrchestration(new TaskOrchestrationFactory() { @@ -122,7 +110,6 @@ public TaskActivity create() { @Override public TaskActivity create() { return ctx -> { - String orderJson = ctx.getInput(String.class); // Simulate payment processing sleep(1000); // Simulate processing time return "{\"success\":true, \"transactionId\":\"TXN" + System.currentTimeMillis() + "\"}"; @@ -137,7 +124,6 @@ public TaskActivity create() { @Override public TaskActivity create() { return ctx -> { - String orderJson = ctx.getInput(String.class); // Simulate shipping process sleep(1000); // Simulate processing time return "{\"trackingNumber\":\"TRACK" + System.currentTimeMillis() + "\"}"; @@ -150,14 +136,10 @@ public TaskActivity create() { @Bean public DurableTaskClient durableTaskClient( - DurableTaskProperties properties, - TokenCredential tokenCredential) { + DurableTaskProperties properties) { // Create client using Azure-managed extensions - return DurableTaskSchedulerClientExtensions.createClientBuilder( - properties.getEndpoint(), - properties.getTaskHubName(), - tokenCredential).build(); + return DurableTaskSchedulerClientExtensions.createClientBuilder(properties.getConnectionString()).build(); } } diff --git a/samples/src/main/resources/application.properties b/samples/src/main/resources/application.properties index 53d6a6d0..c019f8ed 100644 --- a/samples/src/main/resources/application.properties +++ b/samples/src/main/resources/application.properties @@ -1,6 +1,5 @@ durable.task.endpoint= -durable.task.hub-name= -durable.task.allow-insecure=false -# Add logging configuration -logging.level.com.microsoft.durabletask=DEBUG -logging.level.root=INFO \ No newline at end of file +durable.task.taskHubName= +durable.task.connection-string= + +server.port=8082 \ No newline at end of file diff --git a/samples/src/main/resources/web-app-to-durable-task-scheduler-sample.http b/samples/src/main/resources/web-app-to-durable-task-scheduler-sample.http index 71d23162..9bb87f76 100644 --- a/samples/src/main/resources/web-app-to-durable-task-scheduler-sample.http +++ b/samples/src/main/resources/web-app-to-durable-task-scheduler-sample.http @@ -1,5 +1,5 @@ ### Variables -@instanceId = dafc6c87-964b-4e99-8aaf-a5d1061de890 +@instanceId = c32752e9-ae3e-42f7-9dc3-0e91277b1286 ### Create a new order POST http://localhost:8082/api/orders From 98cf7eb8b06c9f58af115e476da79ebf530de227 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sun, 23 Mar 2025 12:53:57 -0700 Subject: [PATCH 51/53] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ab2a421..b4ce7e4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ ## placeholder +* DTS Support ([#201](https://github.com/microsoft/durabletask-java/pull/201)) * Add automatic proto file download and commit hash tracking during build ([#207](https://github.com/microsoft/durabletask-java/pull/207)) * Fix infinite loop when use continueasnew after wait external event ([#183](https://github.com/microsoft/durabletask-java/pull/183)) * Fix the issue "Deserialize Exception got swallowed when use anyOf with external event." ([#185](https://github.com/microsoft/durabletask-java/pull/185)) From 8705055a1c1e435bc9ef2b5c9e0919bfbe552491 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Mon, 24 Mar 2025 12:42:01 -0700 Subject: [PATCH 52/53] Add support for Visual Studio Code and Interactive Browser authentication types in DurableTaskSchedulerConnectionString. Update tests to verify new credential types. --- .../DurableTaskSchedulerConnectionString.java | 6 +++ ...ableTaskSchedulerConnectionStringTest.java | 40 +++++++++++++++++++ .../src/main/resources/application.properties | 9 +++-- 3 files changed, 52 insertions(+), 3 deletions(-) diff --git a/azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerConnectionString.java b/azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerConnectionString.java index 01426f8e..7a392d70 100644 --- a/azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerConnectionString.java +++ b/azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerConnectionString.java @@ -15,7 +15,9 @@ import com.azure.identity.AzurePowerShellCredentialBuilder; import com.azure.identity.DefaultAzureCredentialBuilder; import com.azure.identity.EnvironmentCredentialBuilder; +import com.azure.identity.InteractiveBrowserCredentialBuilder; import com.azure.identity.ManagedIdentityCredentialBuilder; +import com.azure.identity.VisualStudioCodeCredentialBuilder; import com.azure.identity.WorkloadIdentityCredentialBuilder; /** @@ -180,6 +182,10 @@ private static Map parseConnectionString(String connectionString return new AzureCliCredentialBuilder().build(); case "azurepowershell": return new AzurePowerShellCredentialBuilder().build(); + case "visualstudiocode": + return new VisualStudioCodeCredentialBuilder().build(); + case "interactivebrowser": + return new InteractiveBrowserCredentialBuilder().build(); case "none": return null; default: diff --git a/azuremanaged/src/test/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerConnectionStringTest.java b/azuremanaged/src/test/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerConnectionStringTest.java index cf8dcf13..1befa4fc 100644 --- a/azuremanaged/src/test/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerConnectionStringTest.java +++ b/azuremanaged/src/test/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerConnectionStringTest.java @@ -224,4 +224,44 @@ public void getCredential_ConfiguresWorkloadIdentityWithAllProperties() { assertNotNull(credential); assertTrue(credential instanceof WorkloadIdentityCredential); } + + @Test + @DisplayName("getCredential should return VisualStudioCodeCredential for VisualStudioCode authentication type") + public void getCredential_ReturnsVisualStudioCodeCredential() { + // Arrange + String connectionString = String.format( + "Endpoint=%s;Authentication=%s;TaskHub=%s", + "https://example.com", "VisualStudioCode", "myTaskHub"); + + // Act + DurableTaskSchedulerConnectionString result = + new DurableTaskSchedulerConnectionString(connectionString); + + // Assert + TokenCredential credential = result.getCredential(); + assertNotNull(credential); + + // Verify the correct credential type is returned + assertTrue(credential instanceof VisualStudioCodeCredential); + } + + @Test + @DisplayName("getCredential should return InteractiveBrowserCredential for InteractiveBrowser authentication type") + public void getCredential_ReturnsInteractiveBrowserCredential() { + // Arrange + String connectionString = String.format( + "Endpoint=%s;Authentication=%s;TaskHub=%s", + "https://example.com", "InteractiveBrowser", "myTaskHub"); + + // Act + DurableTaskSchedulerConnectionString result = + new DurableTaskSchedulerConnectionString(connectionString); + + // Assert + TokenCredential credential = result.getCredential(); + assertNotNull(credential); + + // Verify the correct credential type is returned + assertTrue(credential instanceof InteractiveBrowserCredential); + } } \ No newline at end of file diff --git a/samples/src/main/resources/application.properties b/samples/src/main/resources/application.properties index c019f8ed..171752e1 100644 --- a/samples/src/main/resources/application.properties +++ b/samples/src/main/resources/application.properties @@ -1,5 +1,8 @@ -durable.task.endpoint= -durable.task.taskHubName= -durable.task.connection-string= +durable.task.endpoint=https://dtwbwuks01-gqa9c3ccgbgd.uksouth.durabletask.io +durable.task.taskHubName=dtwbwuks01th01 +durable.task.connection-string=Endpoint=https://dtwbwuks01-gqa9c3ccgbgd.uksouth.durabletask.io;TaskHub=dtwbwuks01th01;Authentication=DefaultAzure; +# Add logging configuration +logging.level.com.microsoft.durabletask=DEBUG +logging.level.root=INFO server.port=8082 \ No newline at end of file From ad72ecaa001fae170218b735b3c5caa07a96e510 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Mon, 24 Mar 2025 12:57:23 -0700 Subject: [PATCH 53/53] Add support for IntelliJ authentication type in DurableTaskSchedulerConnectionString. Update tests to verify the new credential type. --- .../DurableTaskSchedulerConnectionString.java | 3 +++ ...ableTaskSchedulerConnectionStringTest.java | 20 +++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerConnectionString.java b/azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerConnectionString.java index 7a392d70..eb2e622c 100644 --- a/azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerConnectionString.java +++ b/azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerConnectionString.java @@ -15,6 +15,7 @@ import com.azure.identity.AzurePowerShellCredentialBuilder; import com.azure.identity.DefaultAzureCredentialBuilder; import com.azure.identity.EnvironmentCredentialBuilder; +import com.azure.identity.IntelliJCredentialBuilder; import com.azure.identity.InteractiveBrowserCredentialBuilder; import com.azure.identity.ManagedIdentityCredentialBuilder; import com.azure.identity.VisualStudioCodeCredentialBuilder; @@ -184,6 +185,8 @@ private static Map parseConnectionString(String connectionString return new AzurePowerShellCredentialBuilder().build(); case "visualstudiocode": return new VisualStudioCodeCredentialBuilder().build(); + case "intellij": + return new IntelliJCredentialBuilder().build(); case "interactivebrowser": return new InteractiveBrowserCredentialBuilder().build(); case "none": diff --git a/azuremanaged/src/test/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerConnectionStringTest.java b/azuremanaged/src/test/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerConnectionStringTest.java index 1befa4fc..59d974eb 100644 --- a/azuremanaged/src/test/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerConnectionStringTest.java +++ b/azuremanaged/src/test/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerConnectionStringTest.java @@ -264,4 +264,24 @@ public void getCredential_ReturnsInteractiveBrowserCredential() { // Verify the correct credential type is returned assertTrue(credential instanceof InteractiveBrowserCredential); } + + @Test + @DisplayName("getCredential should return IntelliJCredential for IntelliJ authentication type") + public void getCredential_ReturnsIntelliJCredential() { + // Arrange + String connectionString = String.format( + "Endpoint=%s;Authentication=%s;TaskHub=%s", + "https://example.com", "IntelliJ", "myTaskHub"); + + // Act + DurableTaskSchedulerConnectionString result = + new DurableTaskSchedulerConnectionString(connectionString); + + // Assert + TokenCredential credential = result.getCredential(); + assertNotNull(credential); + + // Verify the correct credential type is returned + assertTrue(credential instanceof IntelliJCredential); + } } \ No newline at end of file