Optimize GET /schedules and /current_user_events internal api endpoints (#4169)

# What this PR does
Speed up `GET /schedules` and `GET /current_user_events` internal api
endpoints by reducing number of calls to database

## Which issue(s) this PR closes
Related to https://github.com/grafana/oncall-private/issues/1552

## Checklist

- [ ] Unit, integration, and e2e (if applicable) tests updated
- [x] Documentation added (or `pr:no public docs` PR label added if not
required)
- [x] Added the relevant release notes label (see labels prefixed w/
`release:`). These labels dictate how your PR will
    show up in the autogenerated release notes.
This commit is contained in:
Yulya Artyukhina 2024-04-11 16:46:51 +02:00 committed by GitHub
parent 7c6ccd772c
commit 6a5267b847
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 52 additions and 17 deletions

View file

@ -4,7 +4,7 @@ import operator
import pytz
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Count, OuterRef, Subquery
from django.db.models import Count, OuterRef, Prefetch, Q, Subquery
from django.db.utils import IntegrityError
from django.urls import reverse
from django.utils import dateparse, timezone
@ -34,8 +34,9 @@ from apps.auth_token.auth import PluginAuthentication
from apps.auth_token.constants import SCHEDULE_EXPORT_TOKEN_NAME
from apps.auth_token.models import ScheduleExportAuthToken
from apps.mobile_app.auth import MobileAppAuthTokenAuthentication
from apps.schedules.constants import PREFETCHED_SHIFT_SWAPS
from apps.schedules.ical_utils import get_oncall_users_for_multiple_schedules
from apps.schedules.models import OnCallSchedule
from apps.schedules.models import OnCallSchedule, ShiftSwapRequest
from apps.slack.models import SlackChannel
from apps.slack.tasks import update_slack_user_group_for_schedules
from common.api_helpers.exceptions import BadRequest, Conflict
@ -138,13 +139,29 @@ class ScheduleView(
since self.get_serializer_context() is called multiple times for every instance in the queryset.
"""
current_schedules = self.get_queryset(annotate=False).none()
events_datetime = datetime.datetime.now(datetime.timezone.utc)
if self.action == "list":
# listing page, only get oncall users for current page schedules
current_schedules = self.paginate_queryset(self.filter_queryset(self.get_queryset(annotate=False)))
# listing page, only get oncall users for current page schedules, prefetch shift swap requests
current_schedules = self.filter_queryset(self.get_queryset(annotate=False)).prefetch_related(
self.prefetch_shift_swaps(
queryset=ShiftSwapRequest.objects.filter(
swap_start__lte=events_datetime, swap_end__gte=events_datetime
)
)
)
current_schedules = self.paginate_queryset(current_schedules)
elif self.kwargs.get("pk"):
# if this is a particular schedule detail, only consider it as current
current_schedules = [self.get_object(annotate=False)]
return get_oncall_users_for_multiple_schedules(current_schedules)
return get_oncall_users_for_multiple_schedules(current_schedules, events_datetime)
@staticmethod
def prefetch_shift_swaps(queryset):
return Prefetch(
"shift_swap_requests",
queryset=queryset.select_related("benefactor", "beneficiary").order_by("created_at"),
to_attr=PREFETCHED_SHIFT_SWAPS,
)
def get_serializer_context(self):
context = super().get_serializer_context()
@ -186,8 +203,9 @@ class ScheduleView(
)
if not ignore_filtering_by_available_teams:
queryset = queryset.filter(*self.available_teams_lookup_args).distinct()
if not is_short_request or annotate:
queryset = self._annotate_queryset(queryset)
if not is_short_request:
if annotate:
queryset = self._annotate_queryset(queryset)
queryset = self.serializer_class.setup_eager_loading(queryset)
if filter_by_type:
valid_types = [i for i in filter_by_type if i in SCHEDULE_TYPE_TO_CLASS]
@ -425,8 +443,19 @@ class ScheduleView(
user_tz, starting_date, days = get_date_range_from_request(self.request)
pytz_tz = pytz.timezone(user_tz)
datetime_start = datetime.datetime.combine(starting_date, datetime.time.min, tzinfo=pytz_tz)
schedules = OnCallSchedule.objects.related_to_user(self.request.user)
datetime_end = datetime_start + datetime.timedelta(days=days)
schedules = (
OnCallSchedule.objects.related_to_user(self.request.user)
.select_related("organization")
.prefetch_related(
self.prefetch_shift_swaps(
queryset=ShiftSwapRequest.objects.filter(
Q(swap_start__lt=datetime_start, swap_end__gte=datetime_start)
| Q(swap_start__gte=datetime_start, swap_start__lte=datetime_end)
)
)
)
)
schedules_events = []
is_oncall = False
for schedule in schedules:

View file

@ -29,3 +29,5 @@ EXPORT_WINDOW_DAYS_BEFORE = 15
SCHEDULE_ONCALL_CACHE_KEY_PREFIX = "schedule_oncall_users_"
SCHEDULE_ONCALL_CACHE_TTL = 15 * 60 # 15 minutes in seconds
PREFETCHED_SHIFT_SWAPS = "prefetched_shift_swaps"

View file

@ -101,7 +101,7 @@ def users_in_ical(
@timed_lru_cache(timeout=100)
def memoized_users_in_ical(usernames_from_ical: typing.List[str], organization: "Organization") -> typing.List["User"]:
def memoized_users_in_ical(usernames_from_ical: typing.Tuple[str], organization: "Organization") -> typing.List["User"]:
# using in-memory cache instead of redis to avoid pickling python objects
return users_in_ical(usernames_from_ical, organization)
@ -358,15 +358,13 @@ def list_users_to_notify_from_ical_for_period(
schedule: "OnCallSchedule",
start_datetime: datetime.datetime,
end_datetime: datetime.datetime,
) -> typing.List["User"]:
users_found_in_ical: typing.Sequence["User"] = []
) -> typing.Sequence["User"]:
events = schedule.final_events(start_datetime, end_datetime)
usernames = []
usernames: typing.List[str] = []
for event in events:
usernames += [u["email"] for u in event.get("users", [])]
users_found_in_ical = users_in_ical(usernames, schedule.organization)
return users_found_in_ical
return memoized_users_in_ical(tuple(usernames), schedule.organization)
def get_oncall_users_for_multiple_schedules(

View file

@ -32,6 +32,7 @@ from apps.schedules.constants import (
ICAL_STATUS_CANCELLED,
ICAL_SUMMARY,
ICAL_UID,
PREFETCHED_SHIFT_SWAPS,
)
from apps.schedules.ical_utils import (
EmptyShifts,
@ -465,7 +466,7 @@ class OnCallSchedule(PolymorphicModel):
return events
def filter_swap_requests(
self, datetime_start: datetime.datetime, datetime_end: datetime.time
self, datetime_start: datetime.datetime, datetime_end: datetime.datetime
) -> "RelatedManager['ShiftSwapRequest']":
swap_requests = self.shift_swap_requests.filter( # starting before but ongoing
swap_start__lt=datetime_start, swap_end__gte=datetime_start
@ -716,7 +717,12 @@ class OnCallSchedule(PolymorphicModel):
) -> ScheduleEvents:
"""Apply swap requests details to schedule events."""
# get swaps requests affecting this schedule / time range
swaps = self.filter_swap_requests(datetime_start, datetime_end)
prefetched_swaps = getattr(self, PREFETCHED_SHIFT_SWAPS, None)
swaps = (
prefetched_swaps
if prefetched_swaps is not None
else self.filter_swap_requests(datetime_start, datetime_end)
)
def _insert_event(index: int, event: ScheduleEvent) -> int:
# add event, if any, to events list in the specified index