From 9988add2800f94009e6c86c0ba96c91c59b140aa Mon Sep 17 00:00:00 2001 From: Alexander Moses Date: Thu, 18 Dec 2025 12:04:22 +0000 Subject: [PATCH 1/3] Bug 2006821: Add Job states using TextChoices This change updates the job model to use a fixed set of TextChoices similar to an Enum. This ensures that the operations such as comparisons of Job states involve fixed string constants and are not susceptible to any typos. --- treeherder/etl/jobs.py | 8 +++--- treeherder/etl/taskcluster_pulse/handler.py | 31 +++++++++++---------- treeherder/model/models.py | 12 +++++++- 3 files changed, 32 insertions(+), 19 deletions(-) diff --git a/treeherder/etl/jobs.py b/treeherder/etl/jobs.py index b0f02c6252c..78d8e3b5bfa 100644 --- a/treeherder/etl/jobs.py +++ b/treeherder/etl/jobs.py @@ -67,9 +67,9 @@ def _remove_existing_jobs(data): # - no backwards transitions (running -> pending, pending/running -> unscheduled) # Allowed: unscheduled -> pending/running/completed, pending -> running/completed, running -> completed if ( - current_state == "completed" - or (job["state"] == "pending" and current_state == "running") - or (job["state"] == "unscheduled" and current_state in ("pending", "running")) + current_state == Job.JobState.COMPLETED + or (job["state"] == Job.JobState.PENDING and current_state == Job.JobState.RUNNING) + or (job["state"] == Job.JobState.UNSCHEDULED and current_state in (Job.JobState.PENDING, Job.JobState.RUNNING)) ): continue new_data.append(datum) @@ -513,5 +513,5 @@ def store_job_data(repository, original_data): if superseded_job_guid_placeholders: for job_guid, superseded_by_guid in superseded_job_guid_placeholders: Job.objects.filter(guid=superseded_by_guid).update( - result="superseded", state="completed" + result="superseded", state=Job.JobState.COMPLETED ) diff --git a/treeherder/etl/taskcluster_pulse/handler.py b/treeherder/etl/taskcluster_pulse/handler.py index c9cb3cfa22e..30a344e2f63 100644 --- a/treeherder/etl/taskcluster_pulse/handler.py +++ b/treeherder/etl/taskcluster_pulse/handler.py @@ -12,6 +12,7 @@ from treeherder.etl.schema import get_json_schema from treeherder.etl.taskcluster_pulse.parse_route import parse_route +from treeherder.model.models import Job env = environ.Env() logger = logging.getLogger(__name__) @@ -20,12 +21,12 @@ # Build a mapping from exchange name to task status EXCHANGE_EVENT_MAP = { - "exchange/taskcluster-queue/v1/task-defined": "unscheduled", - "exchange/taskcluster-queue/v1/task-pending": "pending", - "exchange/taskcluster-queue/v1/task-running": "running", - "exchange/taskcluster-queue/v1/task-completed": "completed", - "exchange/taskcluster-queue/v1/task-failed": "failed", - "exchange/taskcluster-queue/v1/task-exception": "exception", + "exchange/taskcluster-queue/v1/task-defined": Job.JobState.UNSCHEDULED, + "exchange/taskcluster-queue/v1/task-pending": Job.JobState.PENDING, + "exchange/taskcluster-queue/v1/task-running": Job.JobState.RUNNING, + "exchange/taskcluster-queue/v1/task-completed": Job.JobState.COMPLETED, + "exchange/taskcluster-queue/v1/task-failed": Job.JobState.FAILED, + "exchange/taskcluster-queue/v1/task-exception": Job.JobState.EXCEPTION, } @@ -36,13 +37,15 @@ class PulseHandlerError(Exception): def state_from_run(job_run): - return "completed" if job_run["state"] in ("exception", "failed") else job_run["state"] + return ( + Job.JobState.COMPLETED if job_run["state"] in ("exception", "failed") else job_run["state"] + ) def result_from_run(job_run): run_to_result = { - "completed": "success", - "failed": "fail", + Job.JobState.COMPLETED: "success", + Job.JobState.FAILED: "fail", } state = job_run["state"] if state in list(run_to_result.keys()): @@ -202,15 +205,15 @@ async def handle_message(message, task_definition=None): if not task_type: raise Exception("Unknown exchange: {exchange}".format(exchange=message["exchange"])) - elif task_type == "unscheduled": + elif task_type == Job.JobState.UNSCHEDULED: jobs.append(handle_task_defined(parsed_route, task, message)) - elif task_type == "pending": + elif task_type == Job.JobState.PENDING: jobs.append(handle_task_pending(parsed_route, task, message)) - elif task_type == "running": + elif task_type == Job.JobState.RUNNING: jobs.append(handle_task_running(parsed_route, task, message)) - elif task_type in ("completed", "failed"): + elif task_type in (Job.JobState.COMPLETED, Job.JobState.FAILED): jobs.append(await handle_task_completed(parsed_route, task, message, session)) - elif task_type == "exception": + elif task_type == Job.JobState.EXCEPTION: jobs.append(await handle_task_exception(parsed_route, task, message, session)) return jobs diff --git a/treeherder/model/models.py b/treeherder/model/models.py index 3b2d04fc134..e5ae520d404 100644 --- a/treeherder/model/models.py +++ b/treeherder/model/models.py @@ -519,6 +519,16 @@ class Job(models.Model): This class represents a build or test job in Treeherder """ + class JobState(models.TextChoices): + """A representation of Job State.""" + + PENDING = "pending" + RUNNING = "running" + COMPLETED = "completed" + FAILED = "failed" + EXCEPTION = "exception" + UNSCHEDULED = "unscheduled" + failures = FailuresQuerySet.as_manager() objects = JobManager() @@ -560,7 +570,7 @@ class Job(models.Model): who = models.CharField(max_length=50) reason = models.CharField(max_length=125) result = models.CharField(max_length=25) - state = models.CharField(max_length=25) + state = models.CharField(max_length=25, choices=JobState.choices, default=JobState.PENDING) submit_time = models.DateTimeField() start_time = models.DateTimeField() From 5a64a55ad2928c5ed446468481ff9604eea4f25a Mon Sep 17 00:00:00 2001 From: Alexander Moses Date: Thu, 18 Dec 2025 12:24:47 +0000 Subject: [PATCH 2/3] Bug 2006821: Fix formatting Fix formatting --- treeherder/etl/jobs.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/treeherder/etl/jobs.py b/treeherder/etl/jobs.py index 78d8e3b5bfa..f81659c9d87 100644 --- a/treeherder/etl/jobs.py +++ b/treeherder/etl/jobs.py @@ -69,7 +69,10 @@ def _remove_existing_jobs(data): if ( current_state == Job.JobState.COMPLETED or (job["state"] == Job.JobState.PENDING and current_state == Job.JobState.RUNNING) - or (job["state"] == Job.JobState.UNSCHEDULED and current_state in (Job.JobState.PENDING, Job.JobState.RUNNING)) + or ( + job["state"] == Job.JobState.UNSCHEDULED + and current_state in (Job.JobState.PENDING, Job.JobState.RUNNING) + ) ): continue new_data.append(datum) From da16d09a1f2a8580abd9fb5c3d54e04720a3007e Mon Sep 17 00:00:00 2001 From: Alexander Moses Date: Thu, 18 Dec 2025 12:39:38 +0000 Subject: [PATCH 3/3] Bug 2006821: Add the migration Added the migration --- .../model/migrations/0049_alter_job_state.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 treeherder/model/migrations/0049_alter_job_state.py diff --git a/treeherder/model/migrations/0049_alter_job_state.py b/treeherder/model/migrations/0049_alter_job_state.py new file mode 100644 index 00000000000..ae12dee0e1b --- /dev/null +++ b/treeherder/model/migrations/0049_alter_job_state.py @@ -0,0 +1,29 @@ +# Generated by Django 5.1.15 on 2025-12-18 12:39 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("model", "0048_alter_failureline_action"), + ] + + operations = [ + migrations.AlterField( + model_name="job", + name="state", + field=models.CharField( + choices=[ + ("pending", "Pending"), + ("running", "Running"), + ("completed", "Completed"), + ("failed", "Failed"), + ("exception", "Exception"), + ("unscheduled", "Unscheduled"), + ], + default="pending", + max_length=25, + ), + ), + ]