From f5a6091c96532b6aa43584a22532f81b060499e4 Mon Sep 17 00:00:00 2001 From: Cameron Dawson Date: Sat, 15 Aug 2026 14:52:59 -0700 Subject: [PATCH] Eliminate lazy repository FK fetches in worker tasks and push health_summary Production logs show chained tracebacks (KeyError: 'repository' as __context__) from Django's ForwardManyToOneDescriptor: model instances loaded without select_related trigger a one-off Repository fetch deep inside long-running celery tasks, where the DB connection may have gone stale since the instance was first loaded (e.g. after slow log downloads), so the lazy fetch fails mid-task instead of failing fast at task start under the normal retry path. Add select_related for the repository FK chain at the task entry-point querysets (log parser, perfherder ingestion, alert generation, classification loader, artifact loading) and in the health_summary revision branch, which was missing the select_related its author branch already had. This also drops a query per task/request. --- treeherder/etl/artifact.py | 2 +- treeherder/etl/classification_loader.py | 6 ++++-- treeherder/log_parser/intermittents.py | 2 +- treeherder/log_parser/tasks.py | 7 ++++++- treeherder/perf/tasks.py | 8 ++++++-- treeherder/webapp/api/push.py | 2 +- 6 files changed, 19 insertions(+), 8 deletions(-) diff --git a/treeherder/etl/artifact.py b/treeherder/etl/artifact.py index ae71325d356..914cd4e2970 100644 --- a/treeherder/etl/artifact.py +++ b/treeherder/etl/artifact.py @@ -83,7 +83,7 @@ def store_job_artifacts(artifact_data): continue try: - job = Job.objects.get(guid=job_guid) + job = Job.objects.select_related("repository").get(guid=job_guid) except Job.DoesNotExist: logger.error("load_job_artifacts: No job_id for guid %s", job_guid) continue diff --git a/treeherder/etl/classification_loader.py b/treeherder/etl/classification_loader.py index 3c8b2e06b38..3416304146e 100644 --- a/treeherder/etl/classification_loader.py +++ b/treeherder/etl/classification_loader.py @@ -120,7 +120,7 @@ def get_push(self, task_route): revision_field = "revision__startswith" if len(revision) < 40 else "revision" filter_kwargs = {"repository": repository, revision_field: revision} - push = Push.objects.get(**filter_kwargs) + push = Push.objects.select_related("repository").get(**filter_kwargs) except Push.DoesNotExist: logger.info("Job with unsupported revision: %s", revision) raise @@ -154,7 +154,9 @@ def autoclassify_failures(self, failures, classification): # Retrieving the relevant Job try: - job = Job.objects.get(taskcluster_metadata__task_id=task["task_id"]) + job = Job.objects.select_related("repository").get( + taskcluster_metadata__task_id=task["task_id"] + ) except Job.DoesNotExist: logger.error( "Job associated to the TC task %s does not exist and could not be autoclassified.", diff --git a/treeherder/log_parser/intermittents.py b/treeherder/log_parser/intermittents.py index 19bd9ed841d..0e739eedaf8 100644 --- a/treeherder/log_parser/intermittents.py +++ b/treeherder/log_parser/intermittents.py @@ -105,7 +105,7 @@ def _check_and_mark_infra(current_job, job_ids, push_ids): def check_and_mark_intermittent(job_id): - current_job = Job.objects.get(id=job_id) + current_job = Job.objects.select_related("repository", "job_type", "push").get(id=job_id) jtname = current_job.job_type.name.strip("-cf") ids = [current_job.push.id] diff --git a/treeherder/log_parser/tasks.py b/treeherder/log_parser/tasks.py index e52714a1433..dfb8e2de4cf 100644 --- a/treeherder/log_parser/tasks.py +++ b/treeherder/log_parser/tasks.py @@ -28,7 +28,12 @@ def parse_logs(job_id, job_log_ids, priority): # Attach task_id/run_id/job_id as GCP log labels to every line emitted while # parsing this job's logs (including deeper failure-line processing). with log_context(**job_log_labels(job), component="log_parser"): - job_logs = JobLog.objects.filter(id__in=job_log_ids, job=job) + # select_related the job/repository chain: the parsers below access + # job_log.job.repository lazily, and by then the log downloads may have + # outlived the task's original DB connection. + job_logs = JobLog.objects.filter(id__in=job_log_ids, job=job).select_related( + "job__repository" + ) if len(job_log_ids) != len(job_logs): logger.warning( diff --git a/treeherder/perf/tasks.py b/treeherder/perf/tasks.py index 1ea1eaf0267..3e93f33b818 100644 --- a/treeherder/perf/tasks.py +++ b/treeherder/perf/tasks.py @@ -14,7 +14,9 @@ @retryable_task(name="generate-alerts", max_retries=10) def generate_alerts(signature_id): newrelic.agent.add_custom_attribute("signature_id", str(signature_id)) - signature = PerformanceSignature.objects.get(id=signature_id) + signature = PerformanceSignature.objects.select_related("repository", "framework").get( + id=signature_id + ) generate_new_alerts_in_series(signature) # Test alert generation is temporarily disabled. # try: @@ -45,7 +47,9 @@ def ingest_perfherder_data(job_id, job_log_ids): newrelic.agent.add_custom_attribute("job_id", str(job_id)) job = Job.objects.get(id=job_id) - job_artifacts = JobLog.objects.filter(id__in=job_log_ids, job=job) + job_artifacts = JobLog.objects.filter(id__in=job_log_ids, job=job).select_related( + "job__repository" + ) if len(job_log_ids) != len(job_artifacts): logger.warning( diff --git a/treeherder/webapp/api/push.py b/treeherder/webapp/api/push.py index 2a780282602..24b4a3c3d06 100644 --- a/treeherder/webapp/api/push.py +++ b/treeherder/webapp/api/push.py @@ -257,7 +257,7 @@ def health_summary(self, request, project): try: pushes = Push.objects.filter( revision__in=revision.split(","), repository__name=project - ) + ).select_related("repository") except Push.DoesNotExist: return Response(f"No push with revision: {revision}", status=HTTP_404_NOT_FOUND) else: