Skip to content

Commit 014b897

Browse files
committed
fix(identity-vault): test our nextPage logic
Jira: IAM-1793
1 parent d46ebd0 commit 014b897

File tree

3 files changed

+84
-71
lines changed

3 files changed

+84
-71
lines changed

python-modules/cis_identity_vault/cis_identity_vault/models/user.py

Lines changed: 66 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -318,76 +318,14 @@ def all(self):
318318
users.extend(response["Items"])
319319
return users
320320

321-
def _last_evaluated_to_friendly(self, last_evaluated_keys):
322-
"""
323-
Received from Dynamo, and serialized into something our clients can
324-
understand (or rather, use: this _should_ be an opaque token to
325-
clients).
326-
327-
When we're paginating through Dynamo, each segment returns a
328-
`LastEvaluatedKey`, which we need to specify as `ExclusiveStartKey` in
329-
subsequent requests. These `ExclusiveStartKey` is segment-specific,
330-
hence the care here to serialize these in the order returned.
331-
332-
* `None`, indicating that we've completely finished, there are no more
333-
results any segment can return;
334-
* `list[Optional[Any]]`, indicating that _some_ segments have results
335-
left.
336-
337-
Our clients' pagination logic (at least as seen by various publishers),
338-
will consider the query over once we return `None`.
339-
"""
340-
if not last_evaluated_keys:
341-
return None
342-
next_page = []
343-
for last_evaluated_key in last_evaluated_keys:
344-
# A signal that the segment is done scanning.
345-
if last_evaluated_key is None:
346-
id = ""
347-
else:
348-
id = last_evaluated_key["id"]["S"]
349-
next_page.append(id)
350-
# If there are at all any segments left with work, then continue.
351-
if any(next_page):
352-
next_page_raw = ",".join(next_page)
353-
return urllib.parse.quote(next_page_raw)
354-
else:
355-
return None
356-
357-
def _next_page_to_dynamodb(self, next_page_raw):
358-
"""
359-
Received from _clients_, and deserialized into something our parallel
360-
Dynamo code understands.
361-
362-
A complication here is that we can't reuse `None`, since that would
363-
cause a segment to start from the beginning. So, we use a sentinel
364-
value of `"done"` to signal to the parallel Dynamo code that we should
365-
skip this segment.
366-
367-
When Dynamo returns `None`, that means _all_ segments are done. If it
368-
returns a `list[Optional[Any]]`, that means that we can still make
369-
progress on some segments.
370-
"""
371-
if not next_page_raw:
372-
return None
373-
next_page = urllib.parse.unquote(next_page_raw)
374-
exclusive_start_keys = []
375-
for last_evaluated_key in next_page.split(","):
376-
if last_evaluated_key == "":
377-
id = "done"
378-
else:
379-
id = {"id": {"S": last_evaluated_key}}
380-
exclusive_start_keys.append(id)
381-
return exclusive_start_keys
382-
383321
def all_filtered(self, connection_method=None, active=None, next_page=None):
384322
"""
385323
@query_filter str login_method
386324
Returns a dict of all users filtered by query_filter
387325
"""
388326

389327
projection_expression = "id, primary_email, user_uuid, active"
390-
next_page = self._next_page_to_dynamodb(next_page)
328+
next_page = next_page_to_dynamodb(next_page)
391329

392330
if connection_method:
393331
logger.debug("No active filter passed. Assuming we need all users.")
@@ -407,7 +345,7 @@ def all_filtered(self, connection_method=None, active=None, next_page=None):
407345
projection_expression=projection_expression,
408346
exclusive_start_keys=next_page,
409347
)
410-
return dict(users=response["users"], nextPage=self._last_evaluated_to_friendly(response.get("nextPage")))
348+
return dict(users=response["users"], nextPage=last_evaluated_to_friendly(response.get("nextPage")))
411349

412350
def find_or_create(self, user_profile):
413351
profilev2 = json.loads(user_profile["profile"])
@@ -684,3 +622,67 @@ def find_or_create(self, user_profile):
684622
else:
685623
result = self.create(user_profile).user_id
686624
return result
625+
626+
627+
def last_evaluated_to_friendly(last_evaluated_keys):
628+
"""
629+
Received from Dynamo, and serialized into something our clients can
630+
understand (or rather, use: this _should_ be an opaque token to
631+
clients).
632+
633+
When we're paginating through Dynamo, each segment returns a
634+
`LastEvaluatedKey`, which we need to specify as `ExclusiveStartKey` in
635+
subsequent requests. These `ExclusiveStartKey` is segment-specific,
636+
hence the care here to serialize these in the order returned.
637+
638+
* `None`, indicating that we've completely finished, there are no more
639+
results any segment can return;
640+
* `list[Optional[Any]]`, indicating that _some_ segments have results
641+
left.
642+
643+
Our clients' pagination logic (at least as seen by various publishers),
644+
will consider the query over once we return `None`.
645+
"""
646+
if not last_evaluated_keys:
647+
return None
648+
next_page = []
649+
for last_evaluated_key in last_evaluated_keys:
650+
# A signal that the segment is done scanning.
651+
if last_evaluated_key is None:
652+
id = ""
653+
else:
654+
id = last_evaluated_key["id"]["S"]
655+
next_page.append(id)
656+
# If there are at all any segments left with work, then continue.
657+
if any(next_page):
658+
next_page_raw = ",".join(next_page)
659+
return urllib.parse.quote(next_page_raw)
660+
else:
661+
return None
662+
663+
664+
def next_page_to_dynamodb(next_page):
665+
"""
666+
Received from _clients_, and deserialized into something our parallel
667+
Dynamo code understands.
668+
669+
A complication here is that we can't reuse `None`, since that would
670+
cause a segment to start from the beginning. So, we use a sentinel
671+
value of `""` to signal to the parallel Dynamo code that we should
672+
skip this segment.
673+
674+
When Dynamo returns `None`, that means _all_ segments are done. If it
675+
returns a `list[Optional[Any]]`, that means that we can still make
676+
progress on some segments.
677+
"""
678+
if not next_page:
679+
return None
680+
next_page_unquoted = urllib.parse.unquote(next_page)
681+
exclusive_start_keys = []
682+
for last_evaluated_key in next_page_unquoted.split(","):
683+
if last_evaluated_key == "":
684+
id = None
685+
else:
686+
id = {"id": {"S": last_evaluated_key}}
687+
exclusive_start_keys.append(id)
688+
return exclusive_start_keys

python-modules/cis_identity_vault/cis_identity_vault/parallel_dynamo.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -60,16 +60,17 @@ def scan(
6060
users = dict()
6161
last_evaluated_keys = [None] * max_segments
6262
threads = []
63+
start = False
6364

64-
# If this is the first request, then we'll receive a None from our
65-
# caller.
65+
# If this is the first request, then we'll receive a None from our caller.
6666
if exclusive_start_keys is None:
67+
start = True
6768
exclusive_start_keys = [None] * max_segments
6869

6970
# When we're continuing, we signal that a segment has no more work to
70-
# complete if it's ESK is "done". If _all_ of the segments have that, then
71+
# complete if it's ESK is `None`. If _all_ of the segments have that, then
7172
# we're at the end of our result set.
72-
elif all(map(lambda esk: esk == "done", exclusive_start_keys)):
73+
if not start and all(map(lambda esk: esk is None, exclusive_start_keys)):
7374
return dict(users=[], nextPage=None)
7475

7576
for thread_id in range(0, max_segments):
@@ -80,9 +81,9 @@ def scan(
8081
logger.critical("Someone may be DOSing us or not doing pagination properly.")
8182
raise
8283

83-
# If we explicitly read a "done", then this is a signal that the
84-
# segment has no more records.
85-
if exclusive_start_key == "done":
84+
# If we started already and read a `None`, then this is a signal that
85+
# the segment has no more records.
86+
if not start and exclusive_start_key is None:
8687
logger.debug(f"skipping thread {thread_id}")
8788
continue
8889

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from cis_identity_vault.models.user import last_evaluated_to_friendly, next_page_to_dynamodb
2+
3+
4+
def test_identity():
5+
expected = [None, None, {"id": {"S": "deadbeef"}}, {"id": {"S": "feedbeef"}}, None, None]
6+
assert expected == next_page_to_dynamodb(last_evaluated_to_friendly(expected))
7+
8+
9+
def test_identity_two():
10+
assert None == next_page_to_dynamodb(last_evaluated_to_friendly(None))

0 commit comments

Comments
 (0)