From f13362ea9729ad9ad81bdc2e5e00a16d4c2d5e32 Mon Sep 17 00:00:00 2001 From: Jennifer Richards Date: Wed, 15 Oct 2025 17:39:19 -0300 Subject: [PATCH 1/4] refactor: speed up get_attendance() --- ietf/meeting/models.py | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/ietf/meeting/models.py b/ietf/meeting/models.py index f3df23e916..9e44df33b7 100644 --- a/ietf/meeting/models.py +++ b/ietf/meeting/models.py @@ -250,25 +250,39 @@ def get_attendance(self): # MeetingRegistration.attended started conflating badge-pickup and session attendance before IETF 114. # We've separated session attendance off to ietf.meeting.Attended, but need to report attendance at older # meetings correctly. - + # + # Looking up by registration and attendance records separately and joining in + # python is far faster than combining the Q objects in the query (~100x). + # Further optimization may be possible, but the queries are tricky... attended_per_meeting_registration = ( Q(registration__meeting=self) & ( Q(registration__attended=True) | Q(registration__checkedin=True) ) ) + attendees_by_reg = set( + Person.objects.filter(attended_per_meeting_registration).values_list( + "pk", flat=True + ) + ) + attended_per_meeting_attended = ( Q(attended__session__meeting=self) # Note that we are not filtering to plenary, wg, or rg sessions # as we do for nomcom eligibility - if picking up a badge (see above) # is good enough, just attending e.g. a training session is also good enough ) - attended = Person.objects.filter( - attended_per_meeting_registration | attended_per_meeting_attended - ).distinct() - - onsite = set(attended.filter(registration__meeting=self, registration__tickets__attendance_type__slug='onsite')) - remote = set(attended.filter(registration__meeting=self, registration__tickets__attendance_type__slug='remote')) + attendees_by_att = set( + Person.objects.filter(attended_per_meeting_attended).values_list( + "pk", flat=True + ) + ) + + attendees = Person.objects.filter( + pk__in=attendees_by_att | attendees_by_reg + ) + onsite = set(attendees.filter(registration__meeting=self, registration__tickets__attendance_type__slug='onsite')) + remote = set(attendees.filter(registration__meeting=self, registration__tickets__attendance_type__slug='remote')) remote.difference_update(onsite) return Attendance( From 741a88cd69bbdf3a10420d689608c9462837f00f Mon Sep 17 00:00:00 2001 From: Jennifer Richards Date: Wed, 15 Oct 2025 18:15:30 -0300 Subject: [PATCH 2/4] fix: avoid cache invalidation by later draft rev --- ietf/meeting/utils.py | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/ietf/meeting/utils.py b/ietf/meeting/utils.py index f6925269aa..48942e8d1e 100644 --- a/ietf/meeting/utils.py +++ b/ietf/meeting/utils.py @@ -1027,10 +1027,35 @@ def generate_proceedings_content(meeting, force_refresh=False): :force_refresh: true to force regeneration and cache refresh """ cache = caches["default"] - cache_version = Document.objects.filter(session__meeting__number=meeting.number).aggregate(Max('time'))["time__max"] - # Include proceedings_final in the bare_key so we'll always reflect that accurately, even at the cost of - # a recomputation in the view - bare_key = f"proceedings.{meeting.number}.{cache_version}.final={meeting.proceedings_final}" + # Build a cache key that changes when materials are modified. For all but drafts, + # use the last modification time of the document. Exclude drafts from this because + # revisions long after the meeting ends will otherwise show up as changes and + # incorrectly invalidate the cache. Instead, include an ordered list of the + # drafts linked to the meeting so adding or removing drafts will trigger a + # recalculation. The list is long but that doesn't matter because we hash it into + # a fixed-length key. Include proceedings_final in the bare_key so we'll always + # reflect that accurately, even at the cost of a recomputation in the view. + meeting_docs = Document.objects.filter(session__meeting__number=meeting.number) + last_materials_update = ( + meeting_docs.exclude(type_id="draft") + .filter(session__meeting__number=meeting.number) + .aggregate(Max("time"))["time__max"] + ) + draft_names = ( + meeting_docs + .filter(type_id="draft") + .order_by("name") + .values_list("name", flat=True) + ) + bare_key = ".".join( + [ + "proceedings", + str(meeting.number), + last_materials_update.isoformat(), + ",".join(draft_names), + f"final={meeting.proceedings_final}", + ] + ) cache_key = sha384(bare_key.encode("utf8")).hexdigest() if not force_refresh: cached_content = cache.get(cache_key, None) From f7adf9aaeaf4817482e438280f6905dbc89b8b21 Mon Sep 17 00:00:00 2001 From: Jennifer Richards Date: Wed, 15 Oct 2025 22:16:01 -0300 Subject: [PATCH 3/4] fix: guard against empty value --- ietf/meeting/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ietf/meeting/utils.py b/ietf/meeting/utils.py index 48942e8d1e..6d7eb04cbe 100644 --- a/ietf/meeting/utils.py +++ b/ietf/meeting/utils.py @@ -1051,7 +1051,7 @@ def generate_proceedings_content(meeting, force_refresh=False): [ "proceedings", str(meeting.number), - last_materials_update.isoformat(), + last_materials_update.isoformat() if last_materials_update else "-", ",".join(draft_names), f"final={meeting.proceedings_final}", ] From c4b2e016eba334c40af32f8648242e7542cab802 Mon Sep 17 00:00:00 2001 From: Jennifer Richards Date: Wed, 15 Oct 2025 22:58:07 -0300 Subject: [PATCH 4/4] feat: freeze cache key for final proceedings --- ietf/meeting/utils.py | 58 ++++++++++++++++++++++++------------------- 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/ietf/meeting/utils.py b/ietf/meeting/utils.py index 6d7eb04cbe..feadb0c7fd 100644 --- a/ietf/meeting/utils.py +++ b/ietf/meeting/utils.py @@ -1027,35 +1027,41 @@ def generate_proceedings_content(meeting, force_refresh=False): :force_refresh: true to force regeneration and cache refresh """ cache = caches["default"] - # Build a cache key that changes when materials are modified. For all but drafts, - # use the last modification time of the document. Exclude drafts from this because - # revisions long after the meeting ends will otherwise show up as changes and - # incorrectly invalidate the cache. Instead, include an ordered list of the - # drafts linked to the meeting so adding or removing drafts will trigger a - # recalculation. The list is long but that doesn't matter because we hash it into - # a fixed-length key. Include proceedings_final in the bare_key so we'll always - # reflect that accurately, even at the cost of a recomputation in the view. - meeting_docs = Document.objects.filter(session__meeting__number=meeting.number) - last_materials_update = ( - meeting_docs.exclude(type_id="draft") - .filter(session__meeting__number=meeting.number) - .aggregate(Max("time"))["time__max"] - ) - draft_names = ( - meeting_docs - .filter(type_id="draft") - .order_by("name") - .values_list("name", flat=True) - ) - bare_key = ".".join( - [ - "proceedings", - str(meeting.number), + key_components = [ + "proceedings", + str(meeting.number), + ] + if meeting.proceedings_final: + # Freeze the cache key once proceedings are finalized. Further changes will + # not be picked up until the cache expires or is refreshed by the + # proceedings_content_refresh_task() + key_components.append("final") + else: + # Build a cache key that changes when materials are modified. For all but drafts, + # use the last modification time of the document. Exclude drafts from this because + # revisions long after the meeting ends will otherwise show up as changes and + # incorrectly invalidate the cache. Instead, include an ordered list of the + # drafts linked to the meeting so adding or removing drafts will trigger a + # recalculation. The list is long but that doesn't matter because we hash it into + # a fixed-length key. + meeting_docs = Document.objects.filter(session__meeting__number=meeting.number) + last_materials_update = ( + meeting_docs.exclude(type_id="draft") + .filter(session__meeting__number=meeting.number) + .aggregate(Max("time"))["time__max"] + ) + draft_names = ( + meeting_docs + .filter(type_id="draft") + .order_by("name") + .values_list("name", flat=True) + ) + key_components += [ last_materials_update.isoformat() if last_materials_update else "-", ",".join(draft_names), - f"final={meeting.proceedings_final}", ] - ) + + bare_key = ".".join(key_components) cache_key = sha384(bare_key.encode("utf8")).hexdigest() if not force_refresh: cached_content = cache.get(cache_key, None)