Skip to content

Commit 394ee2c

Browse files
committed
Fix more issues with pagination
1 parent eedc523 commit 394ee2c

3 files changed

Lines changed: 51 additions & 45 deletions

File tree

api/graphql/types/reservation_unit/filtersets.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -211,8 +211,8 @@ def get_filter_reservable(self, qs: ReservationUnitQuerySet, name: str, value: d
211211
if not calculate_first_reservable_time:
212212
return qs
213213

214-
# Calculate a cache key for pagination
215-
# Pagination does not affect the calculated results (note: ordering does!).
214+
# Calculate a cache key caching results for pagination.
215+
# Pagination arguments do not affect the calculated results (note: ordering does!).
216216
unaffecting = {"first", "last", "offset", "after", "before"}
217217
args = sorted(f"{key}={value}" for key, value in self.data.items() if key not in unaffecting)
218218
cache_key: str = base64.b64encode(",".join(args).encode()).decode()

reservation_units/utils/first_reservable_time_helper/first_reservable_time_helper.py

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ def __init__( # noqa: PLR0915
211211
##############
212212

213213
if pagination_args is not None:
214-
pagination_args["size"] = graphene_settings.RELAY_CONNECTION_MAX_LIMIT
214+
pagination_args["size"] = reservation_unit_queryset.count()
215215
qs_slice = calculate_queryset_slice(**pagination_args)
216216
self.start_offset = qs_slice.start
217217
self.stop_offset = qs_slice.stop
@@ -245,7 +245,7 @@ def __init__( # noqa: PLR0915
245245

246246
self.first_reservable_times = {}
247247
self.reservation_unit_closed_statuses = {}
248-
self.cached_value_validity = {}
248+
self.cached_value_validity: dict[int, datetime.datetime] = {}
249249

250250
# Closed time spans that are shared by all ReservationUnits
251251
self.shared_hard_closed_time_spans = self._get_shared_hard_closed_time_spans()
@@ -277,20 +277,23 @@ def calculate_all_first_reservable_times(self) -> None:
277277
# If we already have cached FRT results for enough reservation units to fill the page
278278
# AND all the previous pages, then we don't need to calculate anything.
279279
# NOTE: This does not support different orderings of the same queryset!
280-
required = (
280+
# TODO: This doesn't work for the last page if it's not full!
281+
cached = (
281282
sum(1 for frt in self.first_reservable_times.values() if frt is not None)
282283
if self.show_only_reservable
283284
else len(self.first_reservable_times)
284285
)
285-
if required >= self.stop_offset:
286+
if cached >= self.stop_offset:
286287
return
287288

288-
# Otherwise, we can still skip looping through the previous pages.
289-
qs = qs[self.start_offset :]
290-
self.start_offset = 0 # Remove offset since we skip the previous pages
289+
# Otherwise, we should still have valid results for the previous pages.
290+
# We can start calculating after the last cached result.
291+
qs = qs[len(self.first_reservable_times) :]
292+
# We also don't need to skip any results that are not already cached.
293+
self.start_offset = 0
291294

292295
# If we don't have valid results, and this is not the first page,
293-
# we should fetch the current and previous pages in on chunk. The next chunk
296+
# we should fetch the current and previous pages in one chunk. The next chunk
294297
# will also be bigger (if needed), but likely small enough (<100) not to cause any problems.
295298
elif self.start_offset > 0:
296299
self.chunk_size = self.stop_offset
@@ -303,15 +306,14 @@ def calculate_all_first_reservable_times(self) -> None:
303306
self.reservation_unit_closed_statuses[reservation_unit.pk] = is_closed
304307
self.first_reservable_times[reservation_unit.pk] = first_reservable_time
305308

306-
# Start offset should exist only if we need to recalculate previous pages.
307-
# -> Don't count the result for the current page.
308-
if self.start_offset > 0:
309-
self.start_offset -= 1
310-
continue
311-
312309
# If we should only show reservable reservation units, then we should count the result for the
313310
# current page only if there is a first reservable time.
314311
if not self.show_only_reservable or first_reservable_time is not None:
312+
# Start offset should exist only if we need to recalculate previous pages.
313+
# -> Don't count the result for the current page.
314+
if self.start_offset > 0:
315+
self.start_offset -= 1
316+
continue
315317
results += 1
316318

317319
# This only really matters when showing only reservable reservation units,
@@ -327,9 +329,9 @@ def _read_cached_results(self) -> bool:
327329
328330
Check that we have valid results for all the previous pages.
329331
If not, we must recalculate results for all previous pages, since they might be different
330-
if one FRT has changed to None or from None to a valid value.
332+
if one FRT has changed from a datetime to None or vice versa.
331333
"""
332-
cached_data: dict[ReservationUnitPK, dict[str, Any]] = json.loads(cache.get(self.cache_key, "{}"))
334+
cached_data: dict[str, dict[str, Any]] = json.loads(cache.get(self.cache_key, "{}"))
333335

334336
has_valid_results_for_previous_pages = bool(cached_data)
335337
for pk, item in cached_data.items():
@@ -339,21 +341,22 @@ def _read_cached_results(self) -> bool:
339341
self.first_reservable_times.clear()
340342
return False
341343

342-
self.reservation_unit_closed_statuses[pk] = cached_result.closed
343-
self.first_reservable_times[pk] = cached_result.frt
344-
self.cached_value_validity[pk] = cached_result.valid_until
344+
self.reservation_unit_closed_statuses[int(pk)] = cached_result.closed
345+
self.first_reservable_times[int(pk)] = cached_result.frt
346+
self.cached_value_validity[int(pk)] = cached_result.valid_until
345347

346348
return has_valid_results_for_previous_pages
347349

348350
def _cache_results(self) -> None:
349-
"""Save the calculated results to the cache."""
350-
cached_data: dict[ReservationUnitPK, dict[str, Any]] = {}
351+
"""Save the calculated FRT results to the cache."""
352+
cached_data: dict[str, dict[str, Any]] = {}
351353
new_valid_until = self.now + datetime.timedelta(minutes=2)
352354

353355
for pk, frt in self.first_reservable_times.items():
354356
is_closed = self.reservation_unit_closed_statuses[pk]
357+
# If FRT was not calculated in this request, use the previous 'valid_until' value.
355358
valid_until = self.cached_value_validity.get(pk, new_valid_until)
356-
cached_data[pk] = CachedReservableTime(frt=frt, closed=is_closed, valid_until=valid_until).to_dict()
359+
cached_data[str(pk)] = CachedReservableTime(frt=frt, closed=is_closed, valid_until=valid_until).to_dict()
357360

358361
cache.set(self.cache_key, json.dumps(cached_data), timeout=120)
359362

tests/test_graphql_api/test_reservation_unit/test_query_first_reservable_time.py

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ def dt(*, year: int = NEXT_YEAR, month: int = 1, day: int = 1, hour: int = 0, mi
7474

7575
@pytest.fixture(autouse=True)
7676
def _clear_cache() -> None:
77-
# Cache needs to be cleared between tests do that
77+
# Cache needs to be cleared between tests so that
7878
# FRT calculations don't reuse results between runs.
7979
try:
8080
cache.clear()
@@ -2513,14 +2513,15 @@ def test__reservation_unit__first_reservable_time__remove_not_reservable(graphql
25132513
# Check that we fetch reservation units in a big chunk which is then filtered
25142514
# instead of fetching chunks of the expected page size.
25152515
# Queries:
2516-
# 1) Fetch reservation units for FRT calculation
2517-
# 2) Fetch hauki resources for FRT calculation
2518-
# 3) Fetch reservable time spans for FRT calculation
2519-
# 4) Fetch application rounds for FRT calculation
2520-
# 5) Fetch affecting time spans for FRT calculation
2521-
# 6) Count reservation units for response
2522-
# 7) Fetch reservation units for response
2523-
response.assert_query_count(7)
2516+
# 1) Count reservation units for FRT calculation
2517+
# 2) Fetch reservation units for FRT calculation
2518+
# 3) Fetch hauki resources for FRT calculation
2519+
# 4) Fetch reservable time spans for FRT calculation
2520+
# 5) Fetch application rounds for FRT calculation
2521+
# 6) Fetch affecting time spans for FRT calculation
2522+
# 7) Count reservation units for response
2523+
# 8) Fetch reservation units for response
2524+
response.assert_query_count(8)
25242525

25252526

25262527
########################################################################################################################
@@ -2580,9 +2581,9 @@ def test__reservation_unit__first_reservable_time__previous_page_cached(graphql,
25802581
assert cached_value[str(reservation_unit.pk)]["closed"] == "False"
25812582

25822583
# Check that there was no additional queries
2583-
response_2.assert_query_count(7)
2584+
response_2.assert_query_count(8)
25842585
# ...and that we were able to skip iterating through the previous pages due to cached results.
2585-
assert "OFFSET 1" in response_2.queries[0]
2586+
assert "OFFSET 1" in response_2.queries[1]
25862587

25872588

25882589
########################################################################################################################
@@ -2631,9 +2632,9 @@ def test__reservation_unit__first_reservable_time__previous_page_not_cached(grap
26312632
assert cached_value[str(reservation_unit.pk)]["closed"] == "False"
26322633

26332634
# Check that there was no additional queries
2634-
response.assert_query_count(7)
2635+
response.assert_query_count(8)
26352636
# We also cannot skip previous pages due to missing cached results.
2636-
assert "OFFSET 1" not in response.queries[0]
2637+
assert "OFFSET 1" not in response.queries[1]
26372638

26382639

26392640
########################################################################################################################
@@ -2693,7 +2694,7 @@ def test__reservation_unit__first_reservable_time__different_filters_dont_share_
26932694
assert len(cached_value) == 1
26942695

26952696
# We couldn't use the cached results, so make database queries as usual.
2696-
response_2.assert_query_count(7)
2697+
response_2.assert_query_count(8)
26972698

26982699

26992700
########################################################################################################################
@@ -2743,9 +2744,10 @@ def test__reservation_unit__first_reservable_time__use_cached_results(graphql, r
27432744

27442745
# Since we used cached results, we didn't need to make database queries.
27452746
# Only make queries for:
2746-
# 1) Count reservation units for response
2747-
# 2) Fetch reservation units for response
2748-
response_2.assert_query_count(2)
2747+
# 1) Count reservation units for FRT calculation
2748+
# 2) Count reservation units for response
2749+
# 3) Fetch reservation units for response
2750+
response_2.assert_query_count(3)
27492751

27502752

27512753
########################################################################################################################
@@ -2800,9 +2802,10 @@ def test__reservation_unit__first_reservable_time__use_cached_results__not_first
28002802

28012803
# Since we used cached results, we didn't need to make database queries.
28022804
# Only make queries for:
2803-
# 1) Count reservation units for response
2804-
# 2) Fetch reservation units for response
2805-
response_3.assert_query_count(2)
2805+
# 1) Count reservation units for FRT calculation
2806+
# 2) Count reservation units for response
2807+
# 3) Fetch reservation units for response
2808+
response_3.assert_query_count(3)
28062809

28072810

28082811
########################################################################################################################
@@ -2860,4 +2863,4 @@ def test__reservation_unit__first_reservable_time__cached_results_not_valid_anym
28602863

28612864
# Since we couldn't use all the cached results,
28622865
# we needed to fetch data from the database for re-calculation.
2863-
response.assert_query_count(7)
2866+
response.assert_query_count(8)

0 commit comments

Comments
 (0)