Skip to content

fix: remove deferred bindings cache #1687

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 1 addition & 42 deletions azure_functions_worker/bindings/meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@

BINDING_REGISTRY = None
DEFERRED_BINDING_REGISTRY = None
deferred_bindings_cache = {}


def _check_http_input_type_annotation(bind_name: str, pytype: type,
Expand Down Expand Up @@ -285,48 +284,8 @@ def deferred_bindings_decode(binding: typing.Any,
datum: typing.Any,
metadata: typing.Any,
function_name: str):
"""
This cache holds deferred binding types (ie. BlobClient, ContainerClient)
That have already been created, so that the worker can reuse the
Previously created type without creating a new one.

For async types, the function_name is needed as a key to differentiate.
This prevents a known SDK issue where reusing a client across functions
can lose the session context and cause an error.

The cache key is based on: param name, type, resource, function_name

If cache is empty or key doesn't exist, deferred_binding_type is None
"""
global deferred_bindings_cache

# Only applies to Event Hub and Service Bus - cannot cache
# These types will always produce different content and are not clients
if (datum.type == "collection_model_binding_data"
or datum.value.source == "AzureEventHubsEventData"
or datum.value.source == "AzureServiceBusReceivedMessage"):
return binding.decode(datum,
trigger_metadata=metadata,
pytype=pytype)

if deferred_bindings_cache.get((pb.name,
pytype,
datum.value.content,
function_name), None) is not None:
return deferred_bindings_cache.get((pb.name,
pytype,
datum.value.content,
function_name))
else:
deferred_binding_type = binding.decode(datum,
trigger_metadata=metadata,
pytype=pytype)

deferred_bindings_cache[(pb.name,
pytype,
datum.value.content,
function_name)] = deferred_binding_type
return deferred_binding_type
return binding.decode(datum, trigger_metadata=metadata, pytype=pytype)


def check_deferred_bindings_enabled(param_anno: type,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -249,41 +249,6 @@ def put_blob_bytes(req: func.HttpRequest, file: func.Out[bytes]) -> str:
return 'OK'


@app.function_name(name="blob_cache")
@app.blob_input(arg_name="cachedClient",
path="python-worker-tests/test-blobclient-triggered.txt",
connection="AzureWebJobsStorage")
@app.route(route="blob_cache")
def blob_cache(req: func.HttpRequest,
cachedClient: blob.BlobClient) -> str:
return func.HttpResponse(repr(cachedClient))


@app.function_name(name="blob_cache2")
@app.blob_input(arg_name="cachedClient",
path="python-worker-tests/test-blobclient-triggered.txt",
connection="AzureWebJobsStorage")
@app.route(route="blob_cache2")
def blob_cache2(req: func.HttpRequest,
cachedClient: blob.BlobClient) -> func.HttpResponse:
return func.HttpResponse(repr(cachedClient))


@app.function_name(name="blob_cache3")
@app.blob_input(arg_name="cachedClient",
path="python-worker-tests/test-blobclient-triggered.txt",
connection="AzureWebJobsStorage")
@app.blob_input(arg_name="cachedClient2",
path="python-worker-tests/test-blobclient-triggered.txt",
connection="AzureWebJobsStorage")
@app.route(route="blob_cache3")
def blob_cache3(req: func.HttpRequest,
cachedClient: blob.BlobClient,
cachedClient2: blob.BlobClient) -> func.HttpResponse:
return func.HttpResponse("Client 1: " + repr(cachedClient)
+ " | Client 2: " + repr(cachedClient2))


@app.function_name(name="invalid_connection_info")
@app.blob_input(arg_name="client",
path="python-worker-tests/test-blobclient-triggered.txt",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,64 +171,6 @@ def test_type_undefined(self):
self.assertEqual(r.status_code, 200)
self.assertEqual(r.text, 'test-data')

@unittest.skipIf(sys.version_info.minor >= 13, "For python 3.13+,"
"the cache is maintained in the ext and TBD.")
def test_caching(self):
'''
The cache returns the same type based on resource and function name.
Two different functions with clients that access the same resource
will have two different clients. This tests that the same client
is returned for each invocation and that the clients are different
between the two functions.
'''

r = self.webhost.request('GET', 'blob_cache')
r2 = self.webhost.request('GET', 'blob_cache2')
self.assertEqual(r.status_code, 200)
self.assertEqual(r2.status_code, 200)
client = r.text
client2 = r2.text
self.assertNotEqual(client, client2)

r = self.webhost.request('GET', 'blob_cache')
r2 = self.webhost.request('GET', 'blob_cache2')
self.assertEqual(r.status_code, 200)
self.assertEqual(r2.status_code, 200)
self.assertEqual(r.text, client)
self.assertEqual(r2.text, client2)
self.assertNotEqual(r.text, r2.text)

r = self.webhost.request('GET', 'blob_cache')
r2 = self.webhost.request('GET', 'blob_cache2')
self.assertEqual(r.status_code, 200)
self.assertEqual(r2.status_code, 200)
self.assertEqual(r.text, client)
self.assertEqual(r2.text, client2)
self.assertNotEqual(r.text, r2.text)

@unittest.skipIf(sys.version_info.minor >= 13, "For python 3.13+,"
"the cache is maintained in the ext and TBD.")
def test_caching_same_resource(self):
'''
The cache returns the same type based on param name.
One functions with two clients that access the same resource
will have two different clients. This tests that the same clients
are returned for each invocation and that the clients are different
between the two bindings.
'''

r = self.webhost.request('GET', 'blob_cache3')
self.assertEqual(r.status_code, 200)
clients = r.text.split(" | ")
self.assertNotEqual(clients[0], clients[1])

r2 = self.webhost.request('GET', 'blob_cache3')
self.assertEqual(r2.status_code, 200)
clients_second_call = r2.text.split(" | ")
self.assertEqual(clients[0], clients_second_call[0])
self.assertEqual(clients[1], clients_second_call[1])
self.assertNotEqual(clients_second_call[0], clients_second_call[1])

def test_failed_client_creation(self):
r = self.webhost.request('GET', 'invalid_connection_info')
# Without the http_v2_enabled default definition, this request would time out.
Expand Down
Loading