Skip to content

Commit 00b8df5

Browse files
authored
Prevent Unnecessary Read Container Calls (#42143)
* fix container cache * update changelog * fix tests
1 parent 928f476 commit 00b8df5

File tree

8 files changed

+88
-28
lines changed

8 files changed

+88
-28
lines changed

sdk/cosmos/azure-cosmos/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#### Breaking Changes
99

1010
#### Bugs Fixed
11+
* Fixed bug where container cache was not being properly updated resulting in unnecessary extra requests. See [PR 42143](https://github.com/Azure/azure-sdk-for-python/pull/42143).
1112

1213
#### Other Changes
1314

sdk/cosmos/azure-cosmos/azure/cosmos/aio/_container.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
"""Create, read, update and delete items in the Azure Cosmos DB SQL API service.
2323
"""
24+
import asyncio # pylint: disable=do-not-import-asyncio
2425
from datetime import datetime
2526
from typing import (Any, Dict, Mapping, Optional, Sequence, Type, Union, List, Tuple, cast, overload, AsyncIterable,
2627
Callable)
@@ -90,6 +91,7 @@ def __init__(
9091
properties: Optional[Dict[str, Any]] = None
9192
) -> None:
9293
self.client_connection = client_connection
94+
self.container_cache_lock = asyncio.Lock()
9395
self.id = id
9496
self.database_link = database_link
9597
self.container_link = "{}/colls/{}".format(database_link, self.id)
@@ -111,7 +113,9 @@ async def _get_properties_with_options(self, options: Optional[Dict[str, Any]] =
111113

112114
async def _get_properties(self, **kwargs: Any) -> Dict[str, Any]:
113115
if self.container_link not in self.client_connection._container_properties_cache:
114-
await self.read(**kwargs)
116+
async with self.container_cache_lock:
117+
if self.container_link not in self.client_connection._container_properties_cache:
118+
await self.read(**kwargs)
115119
return self.client_connection._container_properties_cache[self.container_link]
116120

117121
@property

sdk/cosmos/azure-cosmos/azure/cosmos/container.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
"""Create, read, update and delete items in the Azure Cosmos DB SQL API service.
2323
"""
24+
import threading
2425
import warnings
2526
from datetime import datetime
2627
from typing import Any, Dict, List, Optional, Sequence, Union, Tuple, Mapping, cast, overload, Iterable, Callable
@@ -94,6 +95,7 @@ def __init__(
9495
self.id = id
9596
self.container_link = "{}/colls/{}".format(database_link, self.id)
9697
self.client_connection = client_connection
98+
self.container_cache_lock = threading.Lock()
9799
self._is_system_key: Optional[bool] = None
98100
self._scripts: Optional[ScriptsProxy] = None
99101
if properties:
@@ -112,7 +114,9 @@ def _get_properties_with_options(self, options: Optional[Dict[str, Any]] = None)
112114

113115
def _get_properties(self, **kwargs: Any) -> Dict[str, Any]:
114116
if self.container_link not in self.__get_client_container_caches():
115-
self.read(**kwargs)
117+
with self.container_cache_lock:
118+
if self.container_link not in self.__get_client_container_caches():
119+
self.read(**kwargs)
116120
return self.__get_client_container_caches()[self.container_link]
117121

118122
@property

sdk/cosmos/azure-cosmos/tests/test_config.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# Copyright (c) Microsoft Corporation. All rights reserved.
33

44
import collections
5+
import logging
56
import os
67
import time
78
import unittest
@@ -553,3 +554,16 @@ def create_range(range_min: str, range_max: str, is_min_inclusive: bool = True,
553554

554555
def create_feed_range_in_dict(feed_range):
555556
return FeedRangeInternalEpk(feed_range).to_dict()
557+
558+
559+
class MockHandler(logging.Handler):
560+
561+
def __init__(self):
562+
super(MockHandler, self).__init__()
563+
self.messages = []
564+
565+
def reset(self):
566+
self.messages = []
567+
568+
def emit(self, record):
569+
self.messages.append(record)

sdk/cosmos/azure-cosmos/tests/test_cosmos_http_logging_policy.py

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,6 @@
2323
from mock import Mock # type: ignore
2424

2525

26-
class MockHandler(logging.Handler):
27-
28-
def __init__(self):
29-
super(MockHandler, self).__init__()
30-
self.messages = []
31-
32-
def reset(self):
33-
self.messages = []
34-
35-
def emit(self, record):
36-
self.messages.append(record)
37-
3826
class FilterStatusCode(logging.Filter):
3927
def filter(self, record):
4028
if hasattr(record, 'status_code') and record.status_code >= 400:
@@ -51,7 +39,7 @@ def filter(self, record):
5139
L2_URL: L2}
5240

5341

54-
def create_logger(name: str, mock_handler: MockHandler, level: int = logging.INFO) -> logging.Logger:
42+
def create_logger(name: str, mock_handler: test_config.MockHandler, level: int = logging.INFO) -> logging.Logger:
5543
logger = logging.getLogger(name)
5644
logger.addHandler(mock_handler)
5745
logger.setLevel(level)
@@ -87,9 +75,9 @@ def setUpClass(cls):
8775
"You must specify your Azure Cosmos account values for "
8876
"'masterKey' and 'host' at the top of this class to run the "
8977
"tests.")
90-
cls.mock_handler_default = MockHandler()
91-
cls.mock_handler_diagnostic = MockHandler()
92-
cls.mock_handler_filtered_diagnostic = MockHandler()
78+
cls.mock_handler_default = test_config.MockHandler()
79+
cls.mock_handler_diagnostic = test_config.MockHandler()
80+
cls.mock_handler_filtered_diagnostic = test_config.MockHandler()
9381

9482
# Add filter to the filtered diagnostics handler
9583

@@ -233,7 +221,7 @@ def test_client_settings(self):
233221
multiple_write_locations = True
234222

235223
# Client setup
236-
mock_handler = MockHandler()
224+
mock_handler = test_config.MockHandler()
237225
logger = create_logger("test_logger_client_settings", mock_handler)
238226

239227
custom_transport = FaultInjectionTransport()
@@ -288,7 +276,7 @@ def test_client_settings(self):
288276

289277
def test_activity_id_logging_policy(self):
290278
# Create a mock handler and logger for the new client
291-
self.mock_handler_activity_id = MockHandler()
279+
self.mock_handler_activity_id = test_config.MockHandler()
292280
self.logger_activity_id = create_logger("test_logger_activity_id", self.mock_handler_activity_id)
293281

294282
# Create a new client with the logger and enable diagnostics logging

sdk/cosmos/azure-cosmos/tests/test_cosmos_http_logging_policy_async.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from test_fault_injection_transport_async import TestFaultInjectionTransportAsync
1717

1818
from test_cosmos_http_logging_policy import create_logger, L1, L2, CONFIG, \
19-
get_locations_list, FilterStatusCode, MockHandler
19+
get_locations_list, FilterStatusCode
2020

2121
try:
2222
from unittest.mock import Mock
@@ -42,10 +42,10 @@ def setUpClass(cls):
4242
"tests.")
4343

4444
async def asyncSetUp(self):
45-
self.mock_handler_default = MockHandler()
46-
self.mock_handler_diagnostic = MockHandler()
47-
self.mock_handler_filtered_diagnostic = MockHandler()
48-
self.mock_handler_activity_id = MockHandler()
45+
self.mock_handler_default = test_config.MockHandler()
46+
self.mock_handler_diagnostic = test_config.MockHandler()
47+
self.mock_handler_filtered_diagnostic = test_config.MockHandler()
48+
self.mock_handler_activity_id = test_config.MockHandler()
4949
# Add filter to the filtered diagnostics handler
5050

5151
self.mock_handler_filtered_diagnostic.addFilter(FilterStatusCode())
@@ -208,7 +208,7 @@ async def test_client_settings_async(self):
208208
multiple_write_locations = True
209209

210210
# Client setup
211-
mock_handler = MockHandler()
211+
mock_handler = test_config.MockHandler()
212212
logger = create_logger("test_logger_client_settings", mock_handler)
213213

214214
custom_transport = FaultInjectionTransportAsync()

sdk/cosmos/azure-cosmos/tests/test_crud.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44

55
"""End-to-end test.
66
"""
7-
7+
import logging
8+
import threading
89
import time
910
import unittest
1011
import urllib.parse as urllib
@@ -617,6 +618,33 @@ def test_document_crud(self):
617618
replaced_document['id'],
618619
replaced_document['id'])
619620

621+
def test_read_collection_only_once(self):
622+
# Add filter to the filtered diagnostics handler
623+
mock_handler = test_config.MockHandler()
624+
logger = logging.getLogger("azure.cosmos")
625+
logger.setLevel(logging.INFO)
626+
logger.addHandler(mock_handler)
627+
628+
client = cosmos_client.CosmosClient(self.host, self.masterKey)
629+
database = client.get_database_client(self.configs.TEST_DATABASE_ID)
630+
container = database.get_container_client(self.configs.TEST_SINGLE_PARTITION_CONTAINER_ID)
631+
632+
def upsert_worker():
633+
# Synchronously perform the upsert item operation
634+
container.upsert_item(body={'id': str(uuid.uuid4()), 'name': f'sample-'})
635+
636+
# Perform 10 concurrent upserts
637+
threads = []
638+
for i in range(10):
639+
t = threading.Thread(target=upsert_worker)
640+
threads.append(t)
641+
t.start()
642+
643+
for t in threads:
644+
t.join()
645+
assert sum("'x-ms-thinclient-proxy-resource-type': 'colls'" in response.message for response in mock_handler.messages) == 1
646+
647+
620648
def test_document_upsert(self):
621649
# create database
622650
created_db = self.databaseForTest

sdk/cosmos/azure-cosmos/tests/test_crud_async.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"""End-to-end test.
66
"""
77
import asyncio
8+
import logging
89
import os
910
import time
1011
import unittest
@@ -441,7 +442,7 @@ async def test_document_upsert_async(self):
441442
await created_collection.delete_item(item=upserted_document, partition_key=upserted_document['pk'])
442443
await created_collection.delete_item(item=new_document, partition_key=new_document['pk'])
443444

444-
# read documents after delete and verify count is same as original
445+
# read documents after delete and verify count remains the same
445446
document_list = [document async for document in created_collection.read_all_items()]
446447
assert len(document_list) == before_create_documents_count
447448

@@ -920,6 +921,25 @@ async def test_client_request_timeout_when_connection_retry_configuration_specif
920921
await container.read_item(item=item['id'], partition_key=item['id'])
921922
print('Async Initialization')
922923

924+
async def test_read_collection_only_once_async(self):
925+
# Add filter to the filtered diagnostics handler
926+
mock_handler = test_config.MockHandler()
927+
logger = logging.getLogger("azure.cosmos")
928+
logger.setLevel(logging.INFO)
929+
logger.addHandler(mock_handler)
930+
931+
async with CosmosClient(self.host, self.masterKey) as client:
932+
database = client.get_database_client(self.configs.TEST_DATABASE_ID)
933+
container = database.get_container_client(self.configs.TEST_SINGLE_PARTITION_CONTAINER_ID)
934+
935+
# Perform 10 concurrent upserts
936+
tasks = [
937+
container.upsert_item(body={'id': str(uuid.uuid4()), 'name': f'sample-{i}'})
938+
for i in range(10)
939+
]
940+
await asyncio.gather(*tasks)
941+
assert sum("'x-ms-thinclient-proxy-resource-type': 'colls'" in response.message for response in mock_handler.messages) == 1
942+
923943
# TODO: Skipping this test to debug later
924944
@unittest.skip
925945
async def test_client_connection_retry_configuration_async(self):
@@ -1500,3 +1520,4 @@ async def _mock_execute_function(self, function, *args, **kwargs):
15001520

15011521
if __name__ == '__main__':
15021522
unittest.main()
1523+

0 commit comments

Comments
 (0)