Skip to content

Commit 7a1a1eb

Browse files
author
annie-mac
committed
remove async keyword from changeFeed query in aio package
1 parent 2950e20 commit 7a1a1eb

File tree

8 files changed

+281
-143
lines changed

8 files changed

+281
-143
lines changed

sdk/cosmos/azure-cosmos/azure/cosmos/_change_feed/aio/change_feed_iterable.py

+74-30
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,13 @@
2121

2222
"""Iterable change feed results in the Azure Cosmos database service.
2323
"""
24+
2425
from azure.core.async_paging import AsyncPageIterator
2526

27+
from azure.cosmos import PartitionKey
2628
from azure.cosmos._change_feed.aio.change_feed_fetcher import ChangeFeedFetcherV1, ChangeFeedFetcherV2
2729
from azure.cosmos._change_feed.aio.change_feed_state import ChangeFeedStateV1, ChangeFeedState
28-
from azure.cosmos._utils import is_base64_encoded
30+
from azure.cosmos._utils import is_base64_encoded, is_key_exists_and_not_none
2931

3032

3133
class ChangeFeedIterable(AsyncPageIterator):
@@ -57,40 +59,30 @@ def __init__(
5759
self._options = options
5860
self._fetch_function = fetch_function
5961
self._collection_link = collection_link
62+
self._change_feed_fetcher = None
6063

61-
change_feed_state = self._options.get("changeFeedState")
62-
if not change_feed_state:
63-
raise ValueError("Missing changeFeedState in feed options")
64+
if not is_key_exists_and_not_none(self._options, "changeFeedStateContext"):
65+
raise ValueError("Missing changeFeedStateContext in feed options")
6466

65-
if isinstance(change_feed_state, ChangeFeedStateV1):
66-
if continuation_token:
67-
if is_base64_encoded(continuation_token):
68-
raise ValueError("Incompatible continuation token")
69-
else:
70-
change_feed_state.apply_server_response_continuation(continuation_token)
67+
change_feed_state_context = self._options.pop("changeFeedStateContext")
7168

72-
self._change_feed_fetcher = ChangeFeedFetcherV1(
73-
self._client,
74-
self._collection_link,
75-
self._options,
76-
fetch_function
77-
)
78-
else:
79-
if continuation_token:
80-
if not is_base64_encoded(continuation_token):
81-
raise ValueError("Incompatible continuation token")
69+
continuation = continuation_token if continuation_token is not None else change_feed_state_context.pop("continuation", None)
8270

83-
effective_change_feed_context = {"continuationFeedRange": continuation_token}
84-
effective_change_feed_state = ChangeFeedState.from_json(change_feed_state.container_rid, effective_change_feed_context)
85-
# replace with the effective change feed state
86-
self._options["continuationFeedRange"] = effective_change_feed_state
71+
# analysis and validate continuation token
72+
# there are two types of continuation token we support currently:
73+
# v1 version: the continuation token would just be the _etag,
74+
# which is being returned when customer is using partition_key_range_id,
75+
# which is under deprecation and does not support split/merge
76+
# v2 version: the continuation token will be base64 encoded composition token which includes full change feed state
77+
if continuation is not None:
78+
if is_base64_encoded(continuation):
79+
change_feed_state_context["continuationFeedRange"] = continuation
80+
else:
81+
change_feed_state_context["continuationPkRangeId"] = continuation
82+
83+
self._validate_change_feed_state_context(change_feed_state_context)
84+
self._options["changeFeedStateContext"] = change_feed_state_context
8785

88-
self._change_feed_fetcher = ChangeFeedFetcherV2(
89-
self._client,
90-
self._collection_link,
91-
self._options,
92-
fetch_function
93-
)
9486
super(ChangeFeedIterable, self).__init__(self._fetch_next, self._unpack, continuation_token=continuation_token)
9587

9688
async def _unpack(self, block):
@@ -112,7 +104,59 @@ async def _fetch_next(self, *args): # pylint: disable=unused-argument
112104
:return: List of results.
113105
:rtype: list
114106
"""
107+
if self._change_feed_fetcher is None:
108+
await self._initialize_change_feed_fetcher()
109+
115110
block = await self._change_feed_fetcher.fetch_next_block()
116111
if not block:
117112
raise StopAsyncIteration
118113
return block
114+
115+
async def _initialize_change_feed_fetcher(self):
116+
change_feed_state_context = self._options.pop("changeFeedStateContext")
117+
conn_properties = await change_feed_state_context.pop("containerProperties")
118+
if is_key_exists_and_not_none(change_feed_state_context, "partitionKey"):
119+
change_feed_state_context["partitionKey"] = await change_feed_state_context.pop("partitionKey")
120+
121+
pk_properties = conn_properties.get("partitionKey")
122+
partition_key_definition = PartitionKey(path=pk_properties["paths"], kind=pk_properties["kind"])
123+
124+
change_feed_state =\
125+
ChangeFeedState.from_json(self._collection_link, conn_properties["_rid"], partition_key_definition, change_feed_state_context)
126+
self._options["changeFeedState"] = change_feed_state
127+
128+
if isinstance(change_feed_state, ChangeFeedStateV1):
129+
self._change_feed_fetcher = ChangeFeedFetcherV1(
130+
self._client,
131+
self._collection_link,
132+
self._options,
133+
self._fetch_function
134+
)
135+
else:
136+
self._change_feed_fetcher = ChangeFeedFetcherV2(
137+
self._client,
138+
self._collection_link,
139+
self._options,
140+
self._fetch_function
141+
)
142+
143+
def _validate_change_feed_state_context(self, change_feed_state_context: dict[str, any]) -> None:
144+
145+
if is_key_exists_and_not_none(change_feed_state_context, "continuationPkRangeId"):
146+
# if continuation token is in v1 format, throw exception if feed_range is set
147+
if is_key_exists_and_not_none(change_feed_state_context, "feedRange"):
148+
raise ValueError("feed_range and continuation are incompatible")
149+
elif is_key_exists_and_not_none(change_feed_state_context, "continuationFeedRange"):
150+
# if continuation token is in v2 format, since the token itself contains the full change feed state
151+
# so we will ignore other parameters (including incompatible parameters) if they passed in
152+
pass
153+
else:
154+
# validation when no continuation is passed
155+
exclusive_keys = ["partitionKeyRangeId", "partitionKey", "feedRange"]
156+
count = sum(1 for key in exclusive_keys if
157+
key in change_feed_state_context and change_feed_state_context[key] is not None)
158+
if count > 1:
159+
raise ValueError(
160+
"partition_key_range_id, partition_key, feed_range are exclusive parameters, please only set one of them")
161+
162+

sdk/cosmos/azure-cosmos/azure/cosmos/_change_feed/aio/change_feed_state.py

+39-17
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,12 @@
2929
from abc import ABC, abstractmethod
3030
from typing import Optional, Union, List, Any
3131

32-
from azure.cosmos import http_constants
32+
from azure.cosmos import http_constants, PartitionKey
3333
from azure.cosmos._change_feed.aio.change_feed_start_from import ChangeFeedStartFromETagAndFeedRange, \
3434
ChangeFeedStartFromInternal
3535
from azure.cosmos._change_feed.aio.composite_continuation_token import CompositeContinuationToken
3636
from azure.cosmos._change_feed.aio.feed_range_composite_continuation_token import FeedRangeCompositeContinuation
37+
from azure.cosmos._change_feed.feed_range import FeedRangeEpk, FeedRangePartitionKey, FeedRange
3738
from azure.cosmos._routing.aio.routing_map_provider import SmartRoutingMapProvider
3839
from azure.cosmos._routing.routing_range import Range
3940
from azure.cosmos._utils import is_key_exists_and_not_none
@@ -49,15 +50,22 @@ def populate_feed_options(self, feed_options: dict[str, any]) -> None:
4950
pass
5051

5152
@abstractmethod
52-
async def populate_request_headers(self, routing_provider: SmartRoutingMapProvider, request_headers: dict[str, any]) -> None:
53+
async def populate_request_headers(
54+
self,
55+
routing_provider: SmartRoutingMapProvider,
56+
request_headers: dict[str, any]) -> None:
5357
pass
5458

5559
@abstractmethod
5660
def apply_server_response_continuation(self, continuation: str) -> None:
5761
pass
5862

5963
@staticmethod
60-
def from_json(container_link: str, container_rid: str, data: dict[str, Any]):
64+
def from_json(
65+
container_link: str,
66+
container_rid: str,
67+
partition_key_definition: PartitionKey,
68+
data: dict[str, Any]):
6169
if is_key_exists_and_not_none(data, "partitionKeyRangeId") or is_key_exists_and_not_none(data, "continuationPkRangeId"):
6270
return ChangeFeedStateV1.from_json(container_link, container_rid, data)
6371
else:
@@ -69,11 +77,11 @@ def from_json(container_link: str, container_rid: str, data: dict[str, Any]):
6977
if version is None:
7078
raise ValueError("Invalid base64 encoded continuation string [Missing version]")
7179
elif version == "V2":
72-
return ChangeFeedStateV2.from_continuation(container_link, container_rid, continuation_json)
80+
return ChangeFeedStateV2.from_continuation(container_link, container_rid, partition_key_definition, continuation_json)
7381
else:
7482
raise ValueError("Invalid base64 encoded continuation string [Invalid version]")
7583
# when there is no continuation token, by default construct ChangeFeedStateV2
76-
return ChangeFeedStateV2.from_initial_state(container_link, container_rid, data)
84+
return ChangeFeedStateV2.from_initial_state(container_link, container_rid, partition_key_definition, data)
7785

7886
class ChangeFeedStateV1(ChangeFeedState):
7987
"""Change feed state v1 implementation. This is used when partition key range id is used or the continuation is just simple _etag
@@ -110,7 +118,10 @@ def from_json(cls, container_link: str, container_rid: str, data: dict[str, Any]
110118
data.get("continuationPkRangeId")
111119
)
112120

113-
async def populate_request_headers(self, routing_provider: SmartRoutingMapProvider, headers: dict[str, Any]) -> None:
121+
async def populate_request_headers(
122+
self,
123+
routing_provider: SmartRoutingMapProvider,
124+
headers: dict[str, Any]) -> None:
114125
headers[http_constants.HttpHeaders.AIM] = http_constants.HttpHeaders.IncrementalFeedHeaderValue
115126

116127
# When a merge happens, the child partition will contain documents ordered by LSN but the _ts/creation time
@@ -140,7 +151,8 @@ def __init__(
140151
self,
141152
container_link: str,
142153
container_rid: str,
143-
feed_range: Range,
154+
partition_key_definition: PartitionKey,
155+
feed_range: FeedRange,
144156
change_feed_start_from: ChangeFeedStartFromInternal,
145157
continuation: Optional[FeedRangeCompositeContinuation] = None):
146158

@@ -151,7 +163,9 @@ def __init__(
151163
self._continuation = continuation
152164
if self._continuation is None:
153165
composite_continuation_token_queue = collections.deque()
154-
composite_continuation_token_queue.append(CompositeContinuationToken(self._feed_range, None))
166+
composite_continuation_token_queue.append(CompositeContinuationToken(
167+
self._feed_range.get_normalized_range(partition_key_definition),
168+
None))
155169
self._continuation =\
156170
FeedRangeCompositeContinuation(self._container_rid, self._feed_range, composite_continuation_token_queue)
157171

@@ -168,7 +182,10 @@ def to_dict(self) -> dict[str, Any]:
168182
self.continuation_property_name: self._continuation.to_dict()
169183
}
170184

171-
async def populate_request_headers(self, routing_provider: SmartRoutingMapProvider, headers: dict[str, any]) -> None:
185+
async def populate_request_headers(
186+
self,
187+
routing_provider: SmartRoutingMapProvider,
188+
headers: dict[str, any]) -> None:
172189
headers[http_constants.HttpHeaders.AIM] = http_constants.HttpHeaders.IncrementalFeedHeaderValue
173190

174191
# When a merge happens, the child partition will contain documents ordered by LSN but the _ts/creation time
@@ -224,6 +241,7 @@ def from_continuation(
224241
cls,
225242
container_link: str,
226243
container_rid: str,
244+
partition_key_definition: PartitionKey,
227245
continuation_json: dict[str, Any]) -> 'ChangeFeedStateV2':
228246

229247
container_rid_from_continuation = continuation_json.get(ChangeFeedStateV2.container_rid_property_name)
@@ -244,6 +262,7 @@ def from_continuation(
244262
return ChangeFeedStateV2(
245263
container_link=container_link,
246264
container_rid=container_rid,
265+
partition_key_definition=partition_key_definition,
247266
feed_range=continuation.feed_range,
248267
change_feed_start_from=change_feed_start_from,
249268
continuation=continuation)
@@ -253,26 +272,29 @@ def from_initial_state(
253272
cls,
254273
container_link: str,
255274
collection_rid: str,
275+
partition_key_definition: PartitionKey,
256276
data: dict[str, Any]) -> 'ChangeFeedStateV2':
257277

258278
if is_key_exists_and_not_none(data, "feedRange"):
259279
feed_range_str = base64.b64decode(data["feedRange"]).decode('utf-8')
260280
feed_range_json = json.loads(feed_range_str)
261-
feed_range = Range.ParseFromDict(feed_range_json)
262-
elif is_key_exists_and_not_none(data, "partitionKeyFeedRange"):
263-
feed_range = data["partitionKeyFeedRange"]
281+
feed_range = FeedRangeEpk(Range.ParseFromDict(feed_range_json))
282+
elif is_key_exists_and_not_none(data, "partitionKey"):
283+
feed_range = FeedRangePartitionKey(data["partitionKey"])
264284
else:
265285
# default to full range
266-
feed_range = Range(
267-
"",
268-
"FF",
269-
True,
270-
False)
286+
feed_range = FeedRangeEpk(
287+
Range(
288+
"",
289+
"FF",
290+
True,
291+
False))
271292

272293
change_feed_start_from = ChangeFeedStartFromInternal.from_start_time(data.get("startTime"))
273294
return cls(
274295
container_link=container_link,
275296
container_rid=collection_rid,
297+
partition_key_definition=partition_key_definition,
276298
feed_range=feed_range,
277299
change_feed_start_from=change_feed_start_from,
278300
continuation=None)

sdk/cosmos/azure-cosmos/azure/cosmos/_change_feed/aio/feed_range_composite_continuation_token.py

+27-15
Original file line numberDiff line numberDiff line change
@@ -27,20 +27,21 @@
2727
from typing import Any
2828

2929
from azure.cosmos._change_feed.aio.composite_continuation_token import CompositeContinuationToken
30+
from azure.cosmos._change_feed.feed_range import FeedRange, FeedRangeEpk, FeedRangePartitionKey
3031
from azure.cosmos._routing.aio.routing_map_provider import SmartRoutingMapProvider
3132
from azure.cosmos._routing.routing_range import Range
33+
from azure.cosmos._utils import is_key_exists_and_not_none
3234

3335

3436
class FeedRangeCompositeContinuation(object):
35-
_version_property_name = "V"
36-
_container_rid_property_name = "Rid"
37-
_continuation_property_name = "Continuation"
38-
_feed_range_property_name = "Range"
37+
_version_property_name = "v"
38+
_container_rid_property_name = "rid"
39+
_continuation_property_name = "continuation"
3940

4041
def __init__(
4142
self,
4243
container_rid: str,
43-
feed_range: Range,
44+
feed_range: FeedRange,
4445
continuation: collections.deque[CompositeContinuationToken]):
4546
if container_rid is None:
4647
raise ValueError("container_rid is missing")
@@ -55,38 +56,49 @@ def __init__(
5556
def current_token(self):
5657
return self._current_token
5758

59+
def get_feed_range(self) -> FeedRange:
60+
if isinstance(self._feed_range, FeedRangeEpk):
61+
return FeedRangeEpk(self.current_token.feed_range)
62+
else:
63+
return self._feed_range
64+
5865
def to_dict(self) -> dict[str, Any]:
59-
return {
60-
self._version_property_name: "v1", #TODO: should this start from v2
66+
json_data = {
67+
self._version_property_name: "v2",
6168
self._container_rid_property_name: self._container_rid,
6269
self._continuation_property_name: [childToken.to_dict() for childToken in self._continuation],
63-
self._feed_range_property_name: self._feed_range.to_dict()
6470
}
6571

72+
json_data.update(self._feed_range.to_dict())
73+
return json_data
74+
6675
@classmethod
6776
def from_json(cls, data) -> 'FeedRangeCompositeContinuation':
6877
version = data.get(cls._version_property_name)
6978
if version is None:
7079
raise ValueError(f"Invalid feed range composite continuation token [Missing {cls._version_property_name}]")
71-
if version != "v1":
80+
if version != "v2":
7281
raise ValueError("Invalid feed range composite continuation token [Invalid version]")
7382

7483
container_rid = data.get(cls._container_rid_property_name)
7584
if container_rid is None:
7685
raise ValueError(f"Invalid feed range composite continuation token [Missing {cls._container_rid_property_name}]")
7786

78-
feed_range_data = data.get(cls._feed_range_property_name)
79-
if feed_range_data is None:
80-
raise ValueError(f"Invalid feed range composite continuation token [Missing {cls._feed_range_property_name}]")
81-
feed_range = Range.ParseFromDict(feed_range_data)
82-
8387
continuation_data = data.get(cls._continuation_property_name)
8488
if continuation_data is None:
8589
raise ValueError(f"Invalid feed range composite continuation token [Missing {cls._continuation_property_name}]")
8690
if not isinstance(continuation_data, list) or len(continuation_data) == 0:
8791
raise ValueError(f"Invalid feed range composite continuation token [The {cls._continuation_property_name} must be non-empty array]")
8892
continuation = [CompositeContinuationToken.from_json(child_range_continuation_token) for child_range_continuation_token in continuation_data]
8993

94+
# parsing feed range
95+
if is_key_exists_and_not_none(data, FeedRangeEpk.type_property_name):
96+
feed_range = FeedRangeEpk.from_json({ FeedRangeEpk.type_property_name: data[FeedRangeEpk.type_property_name] })
97+
elif is_key_exists_and_not_none(data, FeedRangePartitionKey.type_property_name):
98+
feed_range = FeedRangePartitionKey.from_json({ FeedRangePartitionKey.type_property_name: data[FeedRangePartitionKey.type_property_name] })
99+
else:
100+
raise ValueError("Invalid feed range composite continuation token [Missing feed range scope]")
101+
90102
return cls(container_rid=container_rid, feed_range=feed_range, continuation=deque(continuation))
91103

92104
async def handle_feed_range_gone(self, routing_provider: SmartRoutingMapProvider, collection_link: str) -> None:
@@ -130,5 +142,5 @@ def apply_not_modified_response(self) -> None:
130142
self._initial_no_result_range = self._current_token.feed_range
131143

132144
@property
133-
def feed_range(self) -> Range:
145+
def feed_range(self) -> FeedRange:
134146
return self._feed_range

0 commit comments

Comments
 (0)