Skip to content

Commit ccd7761

Browse files
committed
fix: precompute global stats for Ion Wrapped
1 parent 392f059 commit ccd7761

6 files changed

Lines changed: 142 additions & 46 deletions

File tree

docs/source/reference_index/apps/wrapped.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ wrapped
88
:toctree: ../../reference
99

1010
apps
11+
management.commands.warm_wrapped_cache
1112
stats
1213
urls
1314
views
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import time
2+
3+
from django.core.management.base import BaseCommand
4+
5+
from intranet.apps.wrapped.stats import (
6+
compute_signup_counts,
7+
compute_unique_activity_counts,
8+
compute_visit_counts,
9+
set_cached_cohort_counts,
10+
)
11+
from intranet.utils.date import get_date_range_this_year, get_school_year_label
12+
13+
14+
class Command(BaseCommand):
15+
help = "Warm Ion Wrapped cohort caches used for percentile/rank calculations."
16+
17+
def add_arguments(self, parser):
18+
parser.add_argument(
19+
"--skip-visits",
20+
action="store_true",
21+
default=False,
22+
help="Warm only 8th period cohort caches and skip the request-log visit distribution.",
23+
)
24+
25+
def warm_distribution(self, label, cache_name, start, end, compute_counts):
26+
self.stdout.write(f"{label}: computing...")
27+
started = time.monotonic()
28+
counts = set_cached_cohort_counts(cache_name, start, end, compute_counts())
29+
elapsed = time.monotonic() - started
30+
self.stdout.write(self.style.SUCCESS(f"{label}: cached {len(counts)} values in {elapsed:.1f}s."))
31+
32+
def handle(self, *args, **options):
33+
start, end = get_date_range_this_year()
34+
start_date = start.date()
35+
end_date = end.date()
36+
year_label = get_school_year_label()
37+
38+
self.stdout.write(f"Warming Ion Wrapped cohort caches for {year_label}.")
39+
self.stdout.write(f"Date range: {start.isoformat()} through {end.isoformat()}")
40+
41+
self.warm_distribution(
42+
"8th period signup counts",
43+
"signup-counts",
44+
start_date,
45+
end_date,
46+
lambda: compute_signup_counts(start_date, end_date),
47+
)
48+
self.warm_distribution(
49+
"Unique activity counts",
50+
"unique-activity-counts",
51+
start_date,
52+
end_date,
53+
lambda: compute_unique_activity_counts(start_date, end_date),
54+
)
55+
56+
if options["skip_visits"]:
57+
self.stdout.write(self.style.WARNING("Skipping Ion visit counts. Visit percentile labels will be omitted until this cache is warmed."))
58+
else:
59+
self.stdout.write("Ion visit counts use request logs and may take noticeably longer than the 8th period caches.")
60+
self.warm_distribution(
61+
"Ion visit counts",
62+
"visit-counts",
63+
start,
64+
end,
65+
lambda: compute_visit_counts(start, end),
66+
)
67+
68+
self.stdout.write(self.style.SUCCESS("Ion Wrapped cohort cache warm complete."))

intranet/apps/wrapped/stats.py

Lines changed: 70 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from django.conf import settings
66
from django.contrib.auth import get_user_model
77
from django.core.cache import cache
8-
from django.db.models import Count, Q
8+
from django.db.models import Count
99
from django.utils import timezone
1010

1111
from ...utils.date import get_date_range_this_year, get_school_year_label
@@ -16,7 +16,7 @@
1616
from ..polls.models import Answer, Poll
1717

1818
LAST_MINUTE_WINDOW = datetime.timedelta(minutes=10)
19-
COHORT_CACHE_TIMEOUT = 60 * 60
19+
COHORT_CACHE_TIMEOUT = None
2020
RANK_BUCKETS = (0.01, 0.1, 1, 5, 20, 50)
2121

2222

@@ -47,15 +47,74 @@ def rank_label(top_percent):
4747
return None
4848

4949

50-
def cached_cohort_counts(name, start, end, compute_counts):
51-
cache_key = f"wrapped:cohort:v1:{name}:{start.isoformat()}:{end.isoformat()}"
52-
counts = cache.get(cache_key)
53-
if counts is None:
54-
counts = list(compute_counts())
55-
cache.set(cache_key, counts, COHORT_CACHE_TIMEOUT)
50+
def cohort_cache_key(name, start, end):
51+
return f"wrapped:cohort:v1:{name}:{start.isoformat()}:{end.isoformat()}"
52+
53+
54+
def get_cached_cohort_counts(name, start, end):
55+
return cache.get(cohort_cache_key(name, start, end))
56+
57+
58+
def set_cached_cohort_counts(name, start, end, counts):
59+
counts = list(counts)
60+
cache.set(cohort_cache_key(name, start, end), counts, COHORT_CACHE_TIMEOUT)
5661
return counts
5762

5863

64+
def student_ids_for_cohort():
65+
return list(get_user_model().objects.get_students().values_list("id", flat=True))
66+
67+
68+
def compute_signup_counts(start_date, end_date):
69+
student_ids = student_ids_for_cohort()
70+
counts_by_user = dict(
71+
EighthSignup.objects.filter(
72+
user_id__in=student_ids,
73+
scheduled_activity__block__date__gte=start_date,
74+
scheduled_activity__block__date__lte=end_date,
75+
)
76+
.values("user_id")
77+
.annotate(
78+
wrapped_count=Count("id"),
79+
)
80+
.values_list("user_id", "wrapped_count")
81+
)
82+
return [counts_by_user.get(student_id, 0) for student_id in student_ids]
83+
84+
85+
def compute_unique_activity_counts(start_date, end_date):
86+
student_ids = student_ids_for_cohort()
87+
counts_by_user = dict(
88+
EighthSignup.objects.filter(
89+
user_id__in=student_ids,
90+
scheduled_activity__block__date__gte=start_date,
91+
scheduled_activity__block__date__lte=end_date,
92+
)
93+
.values("user_id")
94+
.annotate(
95+
wrapped_count=Count(
96+
"scheduled_activity__activity",
97+
distinct=True,
98+
)
99+
)
100+
.values_list("user_id", "wrapped_count")
101+
)
102+
return [counts_by_user.get(student_id, 0) for student_id in student_ids]
103+
104+
105+
def compute_visit_counts(start, end):
106+
student_ids = student_ids_for_cohort()
107+
counts_by_user = dict(
108+
Request.objects.filter(user_id__in=student_ids, timestamp__gte=start, timestamp__lte=end, method="GET")
109+
.exclude(path__startswith="/wrapped")
110+
.exclude(path__startswith="/api")
111+
.values("user_id")
112+
.annotate(wrapped_count=Count("id"))
113+
.values_list("user_id", "wrapped_count")
114+
)
115+
return [counts_by_user.get(student_id, 0) for student_id in student_ids]
116+
117+
59118
def path_area(path):
60119
clean_path = urlsplit(path).path
61120
if clean_path in ("", "/"):
@@ -177,30 +236,8 @@ def build_eighth_stats(user, start_date, end_date):
177236
for sponsor in sponsors:
178237
sponsor_counts[sponsor.name] += 1
179238

180-
students = get_user_model().objects.get_students()
181-
signup_counts = cached_cohort_counts(
182-
"signup-counts",
183-
start_date,
184-
end_date,
185-
lambda: students.annotate(
186-
wrapped_count=Count(
187-
"eighthsignup",
188-
filter=Q(eighthsignup__scheduled_activity__block__date__gte=start_date, eighthsignup__scheduled_activity__block__date__lte=end_date),
189-
)
190-
).values_list("wrapped_count", flat=True),
191-
)
192-
unique_counts = cached_cohort_counts(
193-
"unique-activity-counts",
194-
start_date,
195-
end_date,
196-
lambda: students.annotate(
197-
wrapped_count=Count(
198-
"eighthsignup__scheduled_activity__activity",
199-
filter=Q(eighthsignup__scheduled_activity__block__date__gte=start_date, eighthsignup__scheduled_activity__block__date__lte=end_date),
200-
distinct=True,
201-
)
202-
).values_list("wrapped_count", flat=True),
203-
)
239+
signup_counts = get_cached_cohort_counts("signup-counts", start_date, end_date)
240+
unique_counts = get_cached_cohort_counts("unique-activity-counts", start_date, end_date)
204241

205242
return {
206243
"total": total,
@@ -243,20 +280,7 @@ def build_usage_stats(user, start, end):
243280
month_counts[local_time.strftime("%B")] += 1
244281
hour_counts[local_time.hour] += 1
245282

246-
visit_filter = (
247-
Q(request__timestamp__gte=start, request__timestamp__lte=end, request__method="GET")
248-
& ~Q(request__path__startswith="/wrapped")
249-
& ~Q(request__path__startswith="/api")
250-
)
251-
visit_counts = cached_cohort_counts(
252-
"visit-counts",
253-
start,
254-
end,
255-
lambda: get_user_model()
256-
.objects.get_students()
257-
.annotate(wrapped_count=Count("request", filter=visit_filter))
258-
.values_list("wrapped_count", flat=True),
259-
)
283+
visit_counts = get_cached_cohort_counts("visit-counts", start, end)
260284

261285
busiest_hour = None
262286
if hour_counts:

intranet/settings/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -726,6 +726,7 @@ def get_oidc_private_key():
726726
"intranet.apps.features",
727727
"intranet.apps.oauth",
728728
"intranet.apps.logs",
729+
"intranet.apps.wrapped",
729730
# Django plugins
730731
"widget_tweaks",
731732
"oauth2_provider", # django-oauth-toolkit

0 commit comments

Comments
 (0)