Skip to content

Commit 49eaa34

Browse files
committed
Cache sync Functions clients
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 76f2163a-0773-4889-b627-1bd4e5c7bea0
1 parent 12bd085 commit 49eaa34

3 files changed

Lines changed: 38 additions & 3 deletions

File tree

azure-functions-durable/azure/durable_functions/client.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
# Licensed under the MIT License.
33

44
import asyncio
5+
import atexit
56
import json
7+
import threading
68

79
from datetime import datetime, timedelta
810
from typing import Any, Optional, Union
@@ -30,6 +32,10 @@
3032
from .internal.compat.purge_history_result import PurgeHistoryResult
3133

3234

35+
_sync_client_cache: dict[str, "SyncDurableFunctionsClient"] = {}
36+
_sync_client_cache_lock = threading.Lock()
37+
38+
3339
# Client class used for Durable Functions
3440
class DurableFunctionsClient(AsyncTaskHubGrpcClient):
3541
"""A gRPC client passed to Durable Functions durable client bindings.
@@ -527,6 +533,21 @@ def __init__(self, client_as_string: str):
527533
channel_options=channel_options,
528534
data_converter=DEFAULT_FUNCTIONS_DATA_CONVERTER)
529535

536+
@classmethod
537+
def get_cached(cls, client_as_string: str) -> "SyncDurableFunctionsClient":
538+
"""Get the process-wide client for a durable-client binding configuration.
539+
540+
Synchronous Functions bindings can be invoked frequently by history
541+
export fan-out activities. Reusing the gRPC channel avoids creating and
542+
tearing down a channel for every activity invocation.
543+
"""
544+
with _sync_client_cache_lock:
545+
cached = _sync_client_cache.get(client_as_string)
546+
if cached is None:
547+
cached = cls(client_as_string)
548+
_sync_client_cache[client_as_string] = cached
549+
return cached
550+
530551
def _parse_client_configuration(self, client_as_string: str) -> None:
531552
client = json.loads(client_as_string)
532553
self.taskHubName = client.get("taskHubName") or ""
@@ -585,3 +606,15 @@ def _get_instance_status_url(
585606
f"/runtime/webhooks/durabletask/instances/{encoded_instance_id}")
586607
base_url = self.baseUrl.rstrip("/") if self.baseUrl else ""
587608
return f"{base_url}/instances/{encoded_instance_id}"
609+
610+
611+
def _close_cached_sync_clients() -> None:
612+
"""Release process-wide synchronous durable-client channels on shutdown."""
613+
with _sync_client_cache_lock:
614+
clients = list(_sync_client_cache.values())
615+
_sync_client_cache.clear()
616+
for client in clients:
617+
client.close()
618+
619+
620+
atexit.register(_close_cached_sync_clients)

azure-functions-durable/azure/durable_functions/decorators/durable_app.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -415,7 +415,7 @@ async def client_bound(*args: Any, **kwargs: Any) -> Any:
415415
if not isinstance(raw_client, str):
416416
raise TypeError(
417417
f"durable client binding '{client_name}' did not provide its configuration")
418-
client = (SyncDurableFunctionsClient(raw_client) if sync
418+
client = (SyncDurableFunctionsClient.get_cached(raw_client) if sync
419419
else DurableFunctionsClient(raw_client))
420420
bound.arguments[client_name] = client
421421
try:
@@ -424,8 +424,6 @@ async def client_bound(*args: Any, **kwargs: Any) -> Any:
424424
finally:
425425
if isinstance(client, DurableFunctionsClient):
426426
client.schedule_close()
427-
else:
428-
client.close()
429427

430428
client_bound.__annotations__[client_name] = str
431429
setattr(client_bound, "client_function", function)

tests/azure-functions-durable/test_decorator_compat.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,12 +135,16 @@ async def starter(client):
135135

136136
async def test_durable_client_input_sync_injects_sync_client():
137137
app = df.DFApp()
138+
clients = []
138139

139140
def starter(client):
141+
clients.append(client)
140142
return type(client).__name__
141143

142144
fb = app.durable_client_input_sync(client_name="client")(starter)
143145
assert await fb._function._func(client="{}") == "SyncDurableFunctionsClient"
146+
assert await fb._function._func(client="{}") == "SyncDurableFunctionsClient"
147+
assert clients[0] is clients[1]
144148

145149

146150
# ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)