Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,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;

Expand Down Expand Up @@ -45,6 +45,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!");
}
Expand All @@ -56,7 +60,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(), null);
return this.client;
}

Expand All @@ -78,9 +82,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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,18 @@ public final class DurableTaskGrpcClient extends DurableTaskClient {
this.sidecarClient = TaskHubSidecarServiceGrpc.newBlockingStub(sidecarGrpcChannel);
}

DurableTaskGrpcClient(int port, String defaultVersion) {
this.dataConverter = new JacksonDataConverter();
this.defaultVersion = defaultVersion;

// 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.
* <p>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Integer, DurableTaskGrpcClient> portToClientMap = new ConcurrentHashMap<>();

public static DurableTaskClient getClient(int port, String defaultVersion) {
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
return portToClientMap.computeIfAbsent(port, DurableTaskGrpcClient::new);
}
}
Comment thread
sophiatev marked this conversation as resolved.
Comment on lines +9 to +19
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The factory creates singleton clients that are never closed during the application lifecycle. While this is intentional to prevent the channel shutdown warnings mentioned in issue #254, it means gRPC channels will remain open until the JVM exits. Consider adding a method to explicitly close all cached clients for scenarios where the application needs to shut down gracefully, such as when running in tests or when the Azure Functions host is shutting down. This would allow proper resource cleanup while maintaining the singleton pattern during normal operation.

Copilot uses AI. Check for mistakes.
Comment thread
sophiatev marked this conversation as resolved.
Loading