From af16d080ec45fe3a672c44e07e8154b6d41a9293 Mon Sep 17 00:00:00 2001 From: Sophia Tevosyan Date: Tue, 16 Dec 2025 14:05:31 -0800 Subject: [PATCH 01/10] starting new release --- CHANGELOG.md | 3 +++ azurefunctions/build.gradle | 2 +- azuremanaged/build.gradle | 2 +- client/build.gradle | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d81ab680..87977e6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ## Unreleased +## v1.6.2 +* Fixing gRPC channel shutdown ([#249](https://github.com/microsoft/durabletask-java/pull/249)) + ## v1.6.1 * Add support for default versions in Durable Function sub-orchestrations ([#241](https://github.com/microsoft/durabletask-java/pull/241)) diff --git a/azurefunctions/build.gradle b/azurefunctions/build.gradle index ab43e553..ca7c1a00 100644 --- a/azurefunctions/build.gradle +++ b/azurefunctions/build.gradle @@ -6,7 +6,7 @@ plugins { } group 'com.microsoft' -version = '1.6.1' +version = '1.6.2' archivesBaseName = 'durabletask-azure-functions' def protocVersion = '3.12.0' diff --git a/azuremanaged/build.gradle b/azuremanaged/build.gradle index 1839bd59..f2e3c53d 100644 --- a/azuremanaged/build.gradle +++ b/azuremanaged/build.gradle @@ -17,7 +17,7 @@ plugins { archivesBaseName = 'durabletask-azuremanaged' group 'com.microsoft' -version = '1.6.1-preview.1' +version = '1.6.2-preview.1' def grpcVersion = '1.59.0' def azureCoreVersion = '1.45.0' diff --git a/client/build.gradle b/client/build.gradle index 3d3769f2..2d6536ad 100644 --- a/client/build.gradle +++ b/client/build.gradle @@ -10,7 +10,7 @@ plugins { } group 'com.microsoft' -version = '1.6.1' +version = '1.6.2' archivesBaseName = 'durabletask-client' def grpcVersion = '1.59.0' From 9ef0448f0d85a68b6331d40108b8d5364f1f8990 Mon Sep 17 00:00:00 2001 From: Sophia Tevosyan Date: Tue, 16 Dec 2025 14:20:02 -0800 Subject: [PATCH 02/10] removed preview flag --- azuremanaged/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azuremanaged/build.gradle b/azuremanaged/build.gradle index f2e3c53d..ac7eaa93 100644 --- a/azuremanaged/build.gradle +++ b/azuremanaged/build.gradle @@ -17,7 +17,7 @@ plugins { archivesBaseName = 'durabletask-azuremanaged' group 'com.microsoft' -version = '1.6.2-preview.1' +version = '1.6.2' def grpcVersion = '1.59.0' def azureCoreVersion = '1.45.0' From 82a3bda82463e83463a6a74e40704fa5d4f18d1f Mon Sep 17 00:00:00 2001 From: Sophia Tevosyan Date: Tue, 27 Jan 2026 23:18:58 -0800 Subject: [PATCH 03/10] first commit --- .../azurefunctions/DurableClientContext.java | 11 +++++++---- .../durabletask/DurableTaskGrpcClient.java | 11 +++++++++++ .../durabletask/DurableTaskGrpcClientFactory.java | 15 +++++++++++++++ 3 files changed, 33 insertions(+), 4 deletions(-) create mode 100644 client/src/main/java/com/microsoft/durabletask/DurableTaskGrpcClientFactory.java diff --git a/azurefunctions/src/main/java/com/microsoft/durabletask/azurefunctions/DurableClientContext.java b/azurefunctions/src/main/java/com/microsoft/durabletask/azurefunctions/DurableClientContext.java index a952db68..975d1a91 100644 --- a/azurefunctions/src/main/java/com/microsoft/durabletask/azurefunctions/DurableClientContext.java +++ b/azurefunctions/src/main/java/com/microsoft/durabletask/azurefunctions/DurableClientContext.java @@ -9,6 +9,7 @@ import com.microsoft.azure.functions.HttpStatus; import com.microsoft.durabletask.DurableTaskClient; import com.microsoft.durabletask.DurableTaskGrpcClientBuilder; +import com.microsoft.durabletask.DurableTaskGrpcClientFactory; import com.microsoft.durabletask.OrchestrationMetadata; import com.microsoft.durabletask.OrchestrationRuntimeStatus; @@ -45,6 +46,10 @@ public String getTaskHubName() { * @return the Durable Task client object associated with the current function invocation. */ public DurableTaskClient getClient() { + if (this.client != null) { + return this.client; + } + if (this.rpcBaseUrl == null || this.rpcBaseUrl.length() == 0) { throw new IllegalStateException("The client context wasn't populated with an RPC base URL!"); } @@ -56,7 +61,7 @@ public DurableTaskClient getClient() { throw new IllegalStateException("The client context RPC base URL was invalid!", ex); } - this.client = new DurableTaskGrpcClientBuilder().port(rpcURL.getPort()).build(); + this.client = DurableTaskGrpcClientFactory.getClient(rpcURL.getPort()); return this.client; } @@ -78,9 +83,7 @@ public HttpResponseMessage waitForCompletionOrCreateCheckStatusResponse( HttpRequestMessage request, String instanceId, Duration timeout) { - if (this.client == null) { - this.client = getClient(); - } + this.client = getClient(); OrchestrationMetadata orchestration; try { orchestration = this.client.waitForInstanceCompletion(instanceId, timeout, true); diff --git a/client/src/main/java/com/microsoft/durabletask/DurableTaskGrpcClient.java b/client/src/main/java/com/microsoft/durabletask/DurableTaskGrpcClient.java index 52d072b8..8f12b3ea 100644 --- a/client/src/main/java/com/microsoft/durabletask/DurableTaskGrpcClient.java +++ b/client/src/main/java/com/microsoft/durabletask/DurableTaskGrpcClient.java @@ -61,6 +61,17 @@ public final class DurableTaskGrpcClient extends DurableTaskClient { this.sidecarClient = TaskHubSidecarServiceGrpc.newBlockingStub(sidecarGrpcChannel); } + DurableTaskGrpcClient(int port) { + this.dataConverter = new JacksonDataConverter(); + + // Need to keep track of this channel so we can dispose it on close() + this.managedSidecarChannel = ManagedChannelBuilder + .forAddress("localhost", port) + .usePlaintext() + .build(); + this.sidecarClient = TaskHubSidecarServiceGrpc.newBlockingStub(this.managedSidecarChannel); + } + /** * Closes the internally managed gRPC channel, if one exists. *

diff --git a/client/src/main/java/com/microsoft/durabletask/DurableTaskGrpcClientFactory.java b/client/src/main/java/com/microsoft/durabletask/DurableTaskGrpcClientFactory.java new file mode 100644 index 00000000..00a7992f --- /dev/null +++ b/client/src/main/java/com/microsoft/durabletask/DurableTaskGrpcClientFactory.java @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.durabletask; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +public final class DurableTaskGrpcClientFactory { + private static final ConcurrentMap portToClientMap = new ConcurrentHashMap<>(); + + public static DurableTaskClient getClient(int port) { + return portToClientMap.computeIfAbsent(port, DurableTaskGrpcClient::new); + } +} \ No newline at end of file From fc9cfcf9029e3739e7525d1a13098a681bd6755c Mon Sep 17 00:00:00 2001 From: Sophia Tevosyan Date: Tue, 27 Jan 2026 23:29:46 -0800 Subject: [PATCH 04/10] fixing the build error --- .../java/com/microsoft/durabletask/DurableTaskGrpcClient.java | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/main/java/com/microsoft/durabletask/DurableTaskGrpcClient.java b/client/src/main/java/com/microsoft/durabletask/DurableTaskGrpcClient.java index 8f12b3ea..21de905c 100644 --- a/client/src/main/java/com/microsoft/durabletask/DurableTaskGrpcClient.java +++ b/client/src/main/java/com/microsoft/durabletask/DurableTaskGrpcClient.java @@ -63,6 +63,7 @@ public final class DurableTaskGrpcClient extends DurableTaskClient { DurableTaskGrpcClient(int port) { this.dataConverter = new JacksonDataConverter(); + this.defaultVersion = null; // Need to keep track of this channel so we can dispose it on close() this.managedSidecarChannel = ManagedChannelBuilder From ae5d802359b7c646cc70012b41d2b922c4540444 Mon Sep 17 00:00:00 2001 From: Sophia Tevosyan Date: Tue, 27 Jan 2026 23:31:44 -0800 Subject: [PATCH 05/10] fixing the build error --- .../durabletask/azurefunctions/DurableClientContext.java | 2 +- .../java/com/microsoft/durabletask/DurableTaskGrpcClient.java | 4 ++-- .../microsoft/durabletask/DurableTaskGrpcClientFactory.java | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/azurefunctions/src/main/java/com/microsoft/durabletask/azurefunctions/DurableClientContext.java b/azurefunctions/src/main/java/com/microsoft/durabletask/azurefunctions/DurableClientContext.java index 975d1a91..329dd215 100644 --- a/azurefunctions/src/main/java/com/microsoft/durabletask/azurefunctions/DurableClientContext.java +++ b/azurefunctions/src/main/java/com/microsoft/durabletask/azurefunctions/DurableClientContext.java @@ -61,7 +61,7 @@ public DurableTaskClient getClient() { throw new IllegalStateException("The client context RPC base URL was invalid!", ex); } - this.client = DurableTaskGrpcClientFactory.getClient(rpcURL.getPort()); + this.client = DurableTaskGrpcClientFactory.getClient(rpcURL.getPort(), null); return this.client; } diff --git a/client/src/main/java/com/microsoft/durabletask/DurableTaskGrpcClient.java b/client/src/main/java/com/microsoft/durabletask/DurableTaskGrpcClient.java index 21de905c..2ebb6ec8 100644 --- a/client/src/main/java/com/microsoft/durabletask/DurableTaskGrpcClient.java +++ b/client/src/main/java/com/microsoft/durabletask/DurableTaskGrpcClient.java @@ -61,9 +61,9 @@ public final class DurableTaskGrpcClient extends DurableTaskClient { this.sidecarClient = TaskHubSidecarServiceGrpc.newBlockingStub(sidecarGrpcChannel); } - DurableTaskGrpcClient(int port) { + DurableTaskGrpcClient(int port, String defaultVersion) { this.dataConverter = new JacksonDataConverter(); - this.defaultVersion = null; + this.defaultVersion = defaultVersion; // Need to keep track of this channel so we can dispose it on close() this.managedSidecarChannel = ManagedChannelBuilder diff --git a/client/src/main/java/com/microsoft/durabletask/DurableTaskGrpcClientFactory.java b/client/src/main/java/com/microsoft/durabletask/DurableTaskGrpcClientFactory.java index 00a7992f..5f21e03c 100644 --- a/client/src/main/java/com/microsoft/durabletask/DurableTaskGrpcClientFactory.java +++ b/client/src/main/java/com/microsoft/durabletask/DurableTaskGrpcClientFactory.java @@ -9,7 +9,7 @@ public final class DurableTaskGrpcClientFactory { private static final ConcurrentMap portToClientMap = new ConcurrentHashMap<>(); - public static DurableTaskClient getClient(int port) { + public static DurableTaskClient getClient(int port, String defaultVersion) { return portToClientMap.computeIfAbsent(port, DurableTaskGrpcClient::new); } } \ No newline at end of file From 39374b015394c164617bdbd2ef3d63effdfaab32 Mon Sep 17 00:00:00 2001 From: sophiatev <38052607+sophiatev@users.noreply.github.com> Date: Tue, 27 Jan 2026 23:32:59 -0800 Subject: [PATCH 06/10] Update azurefunctions/src/main/java/com/microsoft/durabletask/azurefunctions/DurableClientContext.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../durabletask/azurefunctions/DurableClientContext.java | 1 - 1 file changed, 1 deletion(-) diff --git a/azurefunctions/src/main/java/com/microsoft/durabletask/azurefunctions/DurableClientContext.java b/azurefunctions/src/main/java/com/microsoft/durabletask/azurefunctions/DurableClientContext.java index 329dd215..bbf5e198 100644 --- a/azurefunctions/src/main/java/com/microsoft/durabletask/azurefunctions/DurableClientContext.java +++ b/azurefunctions/src/main/java/com/microsoft/durabletask/azurefunctions/DurableClientContext.java @@ -8,7 +8,6 @@ import com.microsoft.azure.functions.HttpStatus; import com.microsoft.durabletask.DurableTaskClient; -import com.microsoft.durabletask.DurableTaskGrpcClientBuilder; import com.microsoft.durabletask.DurableTaskGrpcClientFactory; import com.microsoft.durabletask.OrchestrationMetadata; import com.microsoft.durabletask.OrchestrationRuntimeStatus; From e34a7f9a67d9f055ee695bb6f9457d86ee2b7a57 Mon Sep 17 00:00:00 2001 From: Sophia Tevosyan Date: Tue, 27 Jan 2026 23:37:18 -0800 Subject: [PATCH 07/10] copilot comments --- .../microsoft/durabletask/DurableTaskGrpcClientFactory.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/src/main/java/com/microsoft/durabletask/DurableTaskGrpcClientFactory.java b/client/src/main/java/com/microsoft/durabletask/DurableTaskGrpcClientFactory.java index 5f21e03c..76fb1a8d 100644 --- a/client/src/main/java/com/microsoft/durabletask/DurableTaskGrpcClientFactory.java +++ b/client/src/main/java/com/microsoft/durabletask/DurableTaskGrpcClientFactory.java @@ -9,6 +9,10 @@ public final class DurableTaskGrpcClientFactory { private static final ConcurrentMap portToClientMap = new ConcurrentHashMap<>(); + // Private to prevent instantiation and enforce a singleton pattern + private DurableTaskGrpcClientFactory() { + } + public static DurableTaskClient getClient(int port, String defaultVersion) { return portToClientMap.computeIfAbsent(port, DurableTaskGrpcClient::new); } From 96e02b78ad25e9db25391ba2215c48c78b83736e Mon Sep 17 00:00:00 2001 From: Sophia Tevosyan Date: Tue, 27 Jan 2026 23:39:11 -0800 Subject: [PATCH 08/10] fixing the build errors --- .../com/microsoft/durabletask/DurableTaskGrpcClientFactory.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/main/java/com/microsoft/durabletask/DurableTaskGrpcClientFactory.java b/client/src/main/java/com/microsoft/durabletask/DurableTaskGrpcClientFactory.java index 76fb1a8d..f22a4ba7 100644 --- a/client/src/main/java/com/microsoft/durabletask/DurableTaskGrpcClientFactory.java +++ b/client/src/main/java/com/microsoft/durabletask/DurableTaskGrpcClientFactory.java @@ -14,6 +14,6 @@ private DurableTaskGrpcClientFactory() { } public static DurableTaskClient getClient(int port, String defaultVersion) { - return portToClientMap.computeIfAbsent(port, DurableTaskGrpcClient::new); + return portToClientMap.computeIfAbsent(port, p -> new DurableTaskGrpcClient(p, defaultVersion)); } } \ No newline at end of file From 8377269613751389bd5a070bbe065c9d39eeaf59 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Jan 2026 13:13:36 -0800 Subject: [PATCH 09/10] Add unit tests for DurableTaskGrpcClientFactory (#257) * Initial plan * Add unit tests for DurableTaskGrpcClientFactory and fix compilation error Co-authored-by: sophiatev <38052607+sophiatev@users.noreply.github.com> * Remove changes to DurableTaskGrpcClient.java and proto file, keep only test file Co-authored-by: sophiatev <38052607+sophiatev@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: sophiatev <38052607+sophiatev@users.noreply.github.com> --- .../DurableTaskGrpcClientFactoryTest.java | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 client/src/test/java/com/microsoft/durabletask/DurableTaskGrpcClientFactoryTest.java diff --git a/client/src/test/java/com/microsoft/durabletask/DurableTaskGrpcClientFactoryTest.java b/client/src/test/java/com/microsoft/durabletask/DurableTaskGrpcClientFactoryTest.java new file mode 100644 index 00000000..f77537ad --- /dev/null +++ b/client/src/test/java/com/microsoft/durabletask/DurableTaskGrpcClientFactoryTest.java @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.durabletask; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for DurableTaskGrpcClientFactory. + */ +public class DurableTaskGrpcClientFactoryTest { + + @Test + void getClient_samePort_returnsSameInstance() { + // Arrange + int port = 5001; + + // Act + DurableTaskClient client1 = DurableTaskGrpcClientFactory.getClient(port); + DurableTaskClient client2 = DurableTaskGrpcClientFactory.getClient(port); + + // Assert + assertNotNull(client1, "First client should not be null"); + assertNotNull(client2, "Second client should not be null"); + assertSame(client1, client2, "getClient should return the same instance for the same port"); + } + + @Test + void getClient_differentPorts_returnsDifferentInstances() { + // Arrange + int port1 = 5002; + int port2 = 5003; + + // Act + DurableTaskClient client1 = DurableTaskGrpcClientFactory.getClient(port1); + DurableTaskClient client2 = DurableTaskGrpcClientFactory.getClient(port2); + + // Assert + assertNotNull(client1, "Client for port1 should not be null"); + assertNotNull(client2, "Client for port2 should not be null"); + assertNotSame(client1, client2, "getClient should return different instances for different ports"); + } + + @Test + void getClient_multiplePorts_maintainsCorrectMapping() { + // Arrange + int port1 = 5004; + int port2 = 5005; + int port3 = 5006; + + // Act + DurableTaskClient client1 = DurableTaskGrpcClientFactory.getClient(port1); + DurableTaskClient client2 = DurableTaskGrpcClientFactory.getClient(port2); + DurableTaskClient client3 = DurableTaskGrpcClientFactory.getClient(port3); + + // Request the same ports again + DurableTaskClient client1Again = DurableTaskGrpcClientFactory.getClient(port1); + DurableTaskClient client2Again = DurableTaskGrpcClientFactory.getClient(port2); + DurableTaskClient client3Again = DurableTaskGrpcClientFactory.getClient(port3); + + // Assert + // Verify each port returns the same instance + assertSame(client1, client1Again, "Port " + port1 + " should return the same instance"); + assertSame(client2, client2Again, "Port " + port2 + " should return the same instance"); + assertSame(client3, client3Again, "Port " + port3 + " should return the same instance"); + + // Verify all instances are different from each other + assertNotSame(client1, client2, "Client for port1 and port2 should be different"); + assertNotSame(client1, client3, "Client for port1 and port3 should be different"); + assertNotSame(client2, client3, "Client for port2 and port3 should be different"); + } +} From b30d6cab83c121c8c8c807050345f309c4e74a4e Mon Sep 17 00:00:00 2001 From: Sophia Tevosyan Date: Wed, 28 Jan 2026 13:34:29 -0800 Subject: [PATCH 10/10] fixing the test build errors --- .../DurableTaskGrpcClientFactoryTest.java | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/client/src/test/java/com/microsoft/durabletask/DurableTaskGrpcClientFactoryTest.java b/client/src/test/java/com/microsoft/durabletask/DurableTaskGrpcClientFactoryTest.java index f77537ad..561f1659 100644 --- a/client/src/test/java/com/microsoft/durabletask/DurableTaskGrpcClientFactoryTest.java +++ b/client/src/test/java/com/microsoft/durabletask/DurableTaskGrpcClientFactoryTest.java @@ -12,14 +12,16 @@ */ public class DurableTaskGrpcClientFactoryTest { + private static final String DEFAULT_VERSION = null; + @Test void getClient_samePort_returnsSameInstance() { // Arrange int port = 5001; // Act - DurableTaskClient client1 = DurableTaskGrpcClientFactory.getClient(port); - DurableTaskClient client2 = DurableTaskGrpcClientFactory.getClient(port); + DurableTaskClient client1 = DurableTaskGrpcClientFactory.getClient(port, DEFAULT_VERSION); + DurableTaskClient client2 = DurableTaskGrpcClientFactory.getClient(port, DEFAULT_VERSION); // Assert assertNotNull(client1, "First client should not be null"); @@ -34,8 +36,8 @@ void getClient_differentPorts_returnsDifferentInstances() { int port2 = 5003; // Act - DurableTaskClient client1 = DurableTaskGrpcClientFactory.getClient(port1); - DurableTaskClient client2 = DurableTaskGrpcClientFactory.getClient(port2); + DurableTaskClient client1 = DurableTaskGrpcClientFactory.getClient(port1, DEFAULT_VERSION); + DurableTaskClient client2 = DurableTaskGrpcClientFactory.getClient(port2, DEFAULT_VERSION); // Assert assertNotNull(client1, "Client for port1 should not be null"); @@ -51,14 +53,14 @@ void getClient_multiplePorts_maintainsCorrectMapping() { int port3 = 5006; // Act - DurableTaskClient client1 = DurableTaskGrpcClientFactory.getClient(port1); - DurableTaskClient client2 = DurableTaskGrpcClientFactory.getClient(port2); - DurableTaskClient client3 = DurableTaskGrpcClientFactory.getClient(port3); - + DurableTaskClient client1 = DurableTaskGrpcClientFactory.getClient(port1, DEFAULT_VERSION); + DurableTaskClient client2 = DurableTaskGrpcClientFactory.getClient(port2, DEFAULT_VERSION); + DurableTaskClient client3 = DurableTaskGrpcClientFactory.getClient(port3, DEFAULT_VERSION); + // Request the same ports again - DurableTaskClient client1Again = DurableTaskGrpcClientFactory.getClient(port1); - DurableTaskClient client2Again = DurableTaskGrpcClientFactory.getClient(port2); - DurableTaskClient client3Again = DurableTaskGrpcClientFactory.getClient(port3); + DurableTaskClient client1Again = DurableTaskGrpcClientFactory.getClient(port1, DEFAULT_VERSION); + DurableTaskClient client2Again = DurableTaskGrpcClientFactory.getClient(port2, DEFAULT_VERSION); + DurableTaskClient client3Again = DurableTaskGrpcClientFactory.getClient(port3, DEFAULT_VERSION); // Assert // Verify each port returns the same instance