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,19 @@
// 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<>();

// Private to prevent instantiation and enforce a singleton pattern
private DurableTaskGrpcClientFactory() {
}

public static DurableTaskClient getClient(int port, String defaultVersion) {
return portToClientMap.computeIfAbsent(port, p -> new DurableTaskGrpcClient(p, defaultVersion));
}
}
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.
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// 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 {

private static final String DEFAULT_VERSION = null;

@Test
void getClient_samePort_returnsSameInstance() {
// Arrange
int port = 5001;

// Act
DurableTaskClient client1 = DurableTaskGrpcClientFactory.getClient(port, DEFAULT_VERSION);
DurableTaskClient client2 = DurableTaskGrpcClientFactory.getClient(port, DEFAULT_VERSION);

// 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, DEFAULT_VERSION);
DurableTaskClient client2 = DurableTaskGrpcClientFactory.getClient(port2, DEFAULT_VERSION);

// 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, 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, DEFAULT_VERSION);
DurableTaskClient client2Again = DurableTaskGrpcClientFactory.getClient(port2, DEFAULT_VERSION);
DurableTaskClient client3Again = DurableTaskGrpcClientFactory.getClient(port3, DEFAULT_VERSION);

// 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");
}
}
Loading