From 78b86f28cc8816212edf0ad2c869bb6c0050fb66 Mon Sep 17 00:00:00 2001 From: Alan Peixinho Date: Thu, 25 Jun 2026 17:38:48 -0300 Subject: [PATCH 1/3] fix(metrics): correct dashboard week boundary to Saturday The dashboard window ended on Sunday instead of Saturday, so mid-week it requested a different window than the email and missed the warmed cache key. Align the exclusive end to the most recent Saturday so the page matches the email window and hits the warm cache. Part of #1961 Signed-off-by: Alan Peixinho --- dashboard/src/pages/Metrics/MetricsPage.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/dashboard/src/pages/Metrics/MetricsPage.tsx b/dashboard/src/pages/Metrics/MetricsPage.tsx index 76b799597..fc2d1f925 100644 --- a/dashboard/src/pages/Metrics/MetricsPage.tsx +++ b/dashboard/src/pages/Metrics/MetricsPage.tsx @@ -551,11 +551,8 @@ function LabsSection({ labs }: { labs: LabData[] }): JSX.Element { const getMetricsQueryParams = ( activeDays: number, ): { startDaysAgo: number; endDaysAgo: number } => { - const today = new Date(); - const weekEndDaysAgo = (today.getUTCDay() + 1) % DAYS_IN_WEEK; - const endDaysAgo = Math.max(weekEndDaysAgo - 1, 0); - const startDaysAgo = endDaysAgo + activeDays; - return { startDaysAgo, endDaysAgo }; + const endDaysAgo = (new Date().getUTCDay() + 1) % DAYS_IN_WEEK; + return { startDaysAgo: endDaysAgo + activeDays, endDaysAgo }; }; export const MetricsPage = (): JSX.Element => { From 8aeb0d5bb0301983640180c5c90787f825a17fcc Mon Sep 17 00:00:00 2001 From: Alan Peixinho Date: Thu, 25 Jun 2026 17:39:14 -0300 Subject: [PATCH 2/3] fix(metrics): warm the metrics cache on server startup Warm the cache on web-server startup (gunicorn/runserver only, single-flight via a Redis lock) so a mid-week deploy doesn't leave the page on the short-lived cache until the next Saturday cron. Align the warm window to the latest Saturday so it targets the same Sat-Fri window the dashboard requests instead of a rolling 7-day window. Part of #1961 Signed-off-by: Alan Peixinho --- backend/kernelCI_app/apps.py | 22 ++++++++++++++++++- backend/kernelCI_app/queries/notifications.py | 19 +++++++++++----- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/backend/kernelCI_app/apps.py b/backend/kernelCI_app/apps.py index e233075fd..98ebd64ff 100644 --- a/backend/kernelCI_app/apps.py +++ b/backend/kernelCI_app/apps.py @@ -1,3 +1,7 @@ +import os +import sys +import threading + from django.apps import AppConfig @@ -6,4 +10,20 @@ class KernelciAppConfig(AppConfig): name = "kernelCI_app" def ready(self) -> None: - return super().ready() + super().ready() + is_server = ( + os.path.basename(sys.argv[0]) == "gunicorn" or "runserver" in sys.argv + ) + if not is_server: + return + if "runserver" in sys.argv and os.environ.get("RUN_MAIN") != "true": + return + + from django.core.cache import cache + + from kernelCI_app.queries.notifications import warm_metrics_cache + + # Single-flight via atomic Redis add: only one worker warms per deploy. + if not cache.add("metrics_startup_warm_lock", 1, timeout=600): + return + threading.Thread(target=warm_metrics_cache, daemon=True).start() diff --git a/backend/kernelCI_app/queries/notifications.py b/backend/kernelCI_app/queries/notifications.py index d9527df2f..bde33f450 100644 --- a/backend/kernelCI_app/queries/notifications.py +++ b/backend/kernelCI_app/queries/notifications.py @@ -1,6 +1,6 @@ import sys from concurrent.futures import ThreadPoolExecutor -from datetime import datetime, time, timedelta, timezone +from datetime import date, datetime, time, timedelta, timezone from typing import Any from django.db import connection, connections @@ -749,6 +749,12 @@ def query_fetchall_work( return rows +def last_saturday(today: date | None = None) -> date: + """Most recent Saturday UTC (today when today is Saturday).""" + day = today or datetime.now(timezone.utc).date() + return day - timedelta(days=(day.weekday() + 2) % 7) + + def interval_params(start_days_ago: int, end_days_ago: int) -> dict[str, str]: """Build [start_date, end_date) bounds from UTC day offsets.""" today = datetime.now(timezone.utc).date() @@ -1068,16 +1074,17 @@ def get_metrics_data( def warm_metrics_cache() -> None: + today = datetime.now(timezone.utc).date() + end_days_ago = (today - last_saturday(today)).days for period_days in METRICS_CACHE_WARM_PERIODS: out( - "Warming metrics cache for " - f"{period_days}-day period " - f"(start_days_ago={period_days}, end_days_ago=0)" + f"Warming metrics cache for {period_days}-day Sat-Fri period " + f"(end_days_ago={end_days_ago})" ) try: get_metrics_data( - start_days_ago=period_days, - end_days_ago=0, + start_days_ago=end_days_ago + period_days, + end_days_ago=end_days_ago, use_cache=False, cache_timeout=METRICS_CACHE_WARM_TIMEOUT, ) From 4e76cbe673041a5066070f05e35d0dd2cd868781 Mon Sep 17 00:00:00 2001 From: Alan Peixinho Date: Fri, 26 Jun 2026 09:49:36 -0300 Subject: [PATCH 3/3] fix(metrics): drop HTTP cache on metrics endpoint Query-level cache in get_metrics_data is enough; view_cache added a second layer that could serve stale responses after the warmed query cache updates. Closes #1961 Signed-off-by: Alan Peixinho --- backend/kernelCI_app/urls.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/backend/kernelCI_app/urls.py b/backend/kernelCI_app/urls.py index 2ae755fad..a89890862 100644 --- a/backend/kernelCI_app/urls.py +++ b/backend/kernelCI_app/urls.py @@ -8,7 +8,6 @@ ) from kernelCI_app import views -from kernelCI_app.queries.notifications import METRICS_CACHE_TIMEOUT def view_cache(view, timeout: int = settings.CACHE_TIMEOUT): @@ -180,9 +179,5 @@ def view_cache(view, timeout: int = settings.CACHE_TIMEOUT): path("proxy/", views.ProxyView.as_view(), name="proxyView"), path("origins/", views.OriginsView.as_view(), name="originsView"), path("tree-report/", views.TreeReport.as_view(), name="treeReportView"), - path( - "metrics/", - view_cache(views.MetricsView, timeout=METRICS_CACHE_TIMEOUT), - name="metricsView", - ), + path("metrics/", views.MetricsView.as_view(), name="metricsView"), ]