Skip to content

Commit 39251cb

Browse files
committed
YDA-6977: fix get user group function stats
This ensures that we don't exceed the maximum GenQuery size when a non-rodsadmin user is viewing group statistics.
1 parent 331a341 commit 39251cb

3 files changed

Lines changed: 100 additions & 28 deletions

File tree

stats.py

Lines changed: 18 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import groups
1616
from util import *
17+
from util.misc import split_string_list_by_total_length
1718

1819
__all__ = ['api_resource_browse_group_data',
1920
'api_resource_monthly_category_stats',
@@ -406,7 +407,7 @@ def get_resource_monthly_category_stats(ctx: rule.Context) -> Dict:
406407
if group not in group_storage:
407408
# This can happen if we have a group without a matching group collection
408409
log.write(ctx, f"Warning: ignoring group {group} for category statistics, because storage data not found "
409-
+ "(possibly a group without a group collection?)")
410+
+ "(possibly a group without a group collection?)")
410411
continue
411412
elif len(group_storage[group]) == record_count:
412413
group_storage[group].append(0)
@@ -685,7 +686,7 @@ def get_user_groups_for_stats(ctx: rule.Context, search_filter: str = "", user_n
685686
686687
:returns: All groups of current session's user
687688
"""
688-
groups_list = []
689+
groups = set()
689690

690691
user_name = user.name(ctx) if user_name is None else user_name
691692
user_zone = user.zone(ctx) if zone_name is None else zone_name
@@ -696,42 +697,33 @@ def get_user_groups_for_stats(ctx: rule.Context, search_filter: str = "", user_n
696697

697698
if user.is_rodsadmin(ctx, f"{user_name}#{user_zone}"):
698699
# All groups in zone
699-
groups_list = list(genquery.Query(ctx,
700+
groups.update(list(genquery.Query(ctx,
700701
"ORDER(USER_GROUP_NAME)",
701-
group_filter + zone_filter + search_filter))
702+
group_filter + zone_filter + search_filter)))
702703
else:
703704
# Groups the user is member of
704705
user_filter = f"AND USER_NAME = '{user_name}' "
705-
group_member = list(genquery.Query(ctx,
706-
"ORDER(USER_GROUP_NAME)",
707-
group_filter + user_filter + search_filter))
708-
709-
for grp in group_member:
710-
if grp not in groups_list:
711-
groups_list.append(grp)
706+
groups.update(list(genquery.Query(ctx,
707+
"ORDER(USER_GROUP_NAME)",
708+
group_filter + user_filter + search_filter)))
712709

713710
# Groups the user is datamanager of
714711
dmgroup_member = list(genquery.Query(ctx,
715712
"ORDER(USER_GROUP_NAME)",
716713
"USER_GROUP_NAME LIKE 'datamanager-%' " + user_filter))
714+
categories = [group.replace("datamanager-", "", 1) for group in dmgroup_member]
717715

718-
categories = []
719-
for grp in dmgroup_member:
720-
cat = grp.replace("datamanager-", "", 1)
721-
categories.append(cat)
722-
723-
if len(categories) > 0:
724-
quoted_categories = [f"'{e}'" for e in categories]
716+
# Limit batch size so that it does not exceed maximum GenQuery size. We're using 14000 rather
717+
# than 16000 so that we have a bit of slack for other parts of the query, like search filters.
718+
category_batches = split_string_list_by_total_length(categories, 14000, add_item_length=3)
719+
for category_batch in category_batches:
720+
quoted_categories = [f"'{e}'" for e in category_batch]
725721
categories_string = f"({','.join(quoted_categories)})"
726-
group_dm = list(genquery.Query(ctx,
727-
"ORDER(USER_GROUP_NAME)",
728-
group_filter + search_filter + f"AND META_USER_ATTR_NAME = 'category' AND META_USER_ATTR_VALUE IN {categories_string}"))
729-
730-
for grp in group_dm:
731-
if grp not in groups_list:
732-
groups_list.append(grp)
722+
groups.update(list(genquery.Query(ctx,
723+
"ORDER(USER_GROUP_NAME)",
724+
group_filter + search_filter + f"AND META_USER_ATTR_NAME = 'category' AND META_USER_ATTR_VALUE IN {categories_string}")))
733725

734-
return groups_list
726+
return sorted(groups)
735727

736728

737729
def rule_resource_research(rule_args, callback, rei):

unit-tests/test_util_misc.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
sys.path.append('../util')
1212

13-
from misc import check_data_package_system_avus, human_readable_size, last_run_time_acceptable, remove_empty_objects
13+
from misc import check_data_package_system_avus, human_readable_size, last_run_time_acceptable, remove_empty_objects, split_string_list_by_total_length
1414

1515
# AVs of a successfully published data package, that is the first version of the package
1616
avs_success_data_package = {
@@ -232,3 +232,23 @@ def test_remove_empty_objects(self):
232232
self.assertDictEqual(remove_empty_objects(d), OrderedDict({"key1": "value1", "key2": {"key5": "value5"}}))
233233
d = OrderedDict({"key1": "value1", "key2": [{}]})
234234
self.assertDictEqual(remove_empty_objects(d), OrderedDict({"key1": "value1"}))
235+
236+
def test_split_string_list_by_total_length(self):
237+
# Items don't exceed maximum length
238+
self.assertEqual(split_string_list_by_total_length(["abc", "def", "ghi"], 10),
239+
[["abc", "def", "ghi"]])
240+
# Items exceed maximum length
241+
self.assertEqual(split_string_list_by_total_length(["abc", "def", "ghi", "jkl"], 10),
242+
[["abc", "def", "ghi"], ["jkl"]])
243+
# Items don't exceed maximum length with additional length
244+
self.assertEqual(split_string_list_by_total_length(["abc", "def", "ghi"], 15, add_item_length=2),
245+
[["abc", "def", "ghi"]])
246+
# Items exceed maximum length with additional length
247+
self.assertEqual(split_string_list_by_total_length(["abc", "def", "ghi"], 14, add_item_length=2),
248+
[["abc", "def"], ["ghi"]])
249+
# Single item exceeds maximum length
250+
self.assertEqual(split_string_list_by_total_length(["abcabcabcabcabcabcabc", "def", "ghi"], 15),
251+
[["abcabcabcabcabcabcabc"], ["def", "ghi"]])
252+
# Single item exceeds maximum length and throws Exception
253+
with self.assertRaises(Exception): # noqa B107 / Ruff does not permit asserting exceptions in unit tests
254+
split_string_list_by_total_length(["abcabcabcabcabcabcabc", "def", "ghi"], 15, raise_exception_exceed=True)

util/misc.py

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import time
88
import uuid
99
from collections import OrderedDict
10-
from typing import Dict
10+
from typing import Dict, List
1111

1212
import constants
1313

@@ -154,3 +154,63 @@ def is_valid_uuid(uuid_string: str) -> bool:
154154
return False
155155

156156
return str(uuid_obj) == uuid_string
157+
158+
159+
def split_string_list_by_total_length(string_list: List[str], max_length: int, add_item_length: int = 0, raise_exception_exceed: bool = False) -> List[List[str]]:
160+
"""Split a list of strings into sublists where the total length of all strings
161+
in a sublist does not exceed the maximum length. This can be useful when you want
162+
to process a list of strings, but need to take into account a maximum length supported
163+
by iRODS (e.g. for GenQueries).
164+
165+
:param string_list: List of strings to process
166+
:param max_length: The maximum length of each sublist in the output. By default, if a single string
167+
(along with its additional item length, if applicable) exceeds the maximum length,
168+
if it included in a sublist by itself.
169+
:param add_item_length: Additional item length. Increase the length of each string by this number.
170+
This is useful if you need to add additional characters to each string when you
171+
use the sublists (e.g. separator characters or quote characters)
172+
:param raise_exception_exceed: Raise an exception if the length of an individual string (along with
173+
its additional item length) exceeds the maximum length, rather than including the
174+
string in its own sublist.
175+
176+
:raises Exception: if a single string in the input list plus the additional item length exceeds
177+
the maximum length, so that it is not possible to strictly meet the maximum
178+
length requirement. By default, such long strings are included in a sublist
179+
by themselves, and no exception is raised.
180+
181+
:returns: List of sublists, where each sublist is either a single string, or a list of strings
182+
whose total size does not exceed the maximum length.
183+
184+
"""
185+
output: List[List[str]] = []
186+
current_sublist: List[str] = []
187+
current_length: List[int] = [0] # In a list so that we can pass it by reference to subfunctions
188+
189+
def end_of_sublist(current_sublist: List[str], current_length: List[int], output: List[List[str]]) -> None:
190+
if len(current_sublist) > 0:
191+
output.append(current_sublist.copy())
192+
current_sublist.clear()
193+
current_length[0] = 0
194+
195+
def add_to_sublist(item: str, effective_length: int, current_sublist: List[str], current_length: List[int]) -> None:
196+
current_sublist.append(item)
197+
current_length[0] += effective_length
198+
199+
for item in string_list:
200+
effective_length = len(item) + add_item_length
201+
if effective_length > max_length:
202+
if raise_exception_exceed:
203+
raise Exception(f"Item '{item}' exceeded maximum sublist length.")
204+
else:
205+
end_of_sublist(current_sublist, current_length, output)
206+
add_to_sublist(item, effective_length, current_sublist, current_length)
207+
end_of_sublist(current_sublist, current_length, output)
208+
elif current_length[0] + effective_length > max_length:
209+
end_of_sublist(current_sublist, current_length, output)
210+
add_to_sublist(item, effective_length, current_sublist, current_length)
211+
else:
212+
add_to_sublist(item, effective_length, current_sublist, current_length)
213+
214+
end_of_sublist(current_sublist, current_length, output)
215+
216+
return output

0 commit comments

Comments
 (0)