Explorar o código

Ensure correct ordering by elapsed time

Jeremy Stretch hai 2 semanas
pai
achega
bff5ee605a
Modificáronse 3 ficheiros con 27 adicións e 15 borrados
  1. 7 0
      netbox/core/api/views.py
  2. 12 8
      netbox/core/models/jobs.py
  3. 8 7
      netbox/core/tables/jobs.py

+ 7 - 0
netbox/core/api/views.py

@@ -1,6 +1,7 @@
 from django.http import Http404, HttpResponse
 from django.shortcuts import get_object_or_404
 from django.utils.translation import gettext_lazy as _
+from django_filters.rest_framework import DjangoFilterBackend
 from django_rq.queues import get_redis_connection
 from django_rq.settings import get_queues_list
 from django_rq.utils import get_statistics
@@ -19,6 +20,7 @@ from core.jobs import SyncDataSourceJob
 from core.models import *
 from core.utils import delete_rq_job, enqueue_rq_job, get_rq_jobs, requeue_rq_job, stop_rq_job
 from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
+from netbox.api.filter_backends import OrderingFilter
 from netbox.api.metadata import ContentTypeMetadata
 from netbox.api.pagination import LimitOffsetListPagination
 from netbox.api.viewsets import NetBoxModelViewSet, NetBoxReadOnlyModelViewSet
@@ -71,6 +73,11 @@ class JobViewSet(NetBoxReadOnlyModelViewSet):
     queryset = Job.objects.all()
     serializer_class = serializers.JobSerializer
     filterset_class = filtersets.JobFilterSet
+    filter_backends = (DjangoFilterBackend, OrderingFilter)
+    # Order by elapsed time for jobs which are still running, matching the jobs table in the UI
+    ordering_expressions = {
+        'execution_time': Job.elapsed_time_expression(),
+    }
 
 
 class ObjectChangeViewSet(NetBoxReadOnlyModelViewSet):

+ 12 - 8
netbox/core/models/jobs.py

@@ -11,6 +11,8 @@ from django.core.exceptions import ValidationError
 from django.core.serializers.json import DjangoJSONEncoder
 from django.core.validators import MinValueValidator
 from django.db import models, transaction
+from django.db.models import ExpressionWrapper, F
+from django.db.models.functions import Coalesce, Now
 from django.urls import reverse
 from django.utils import timezone
 from django.utils.translation import gettext as _
@@ -194,14 +196,16 @@ class Job(models.Model):
             return timezone.now() - self.started
         return None
 
-    @property
-    def duration(self):
-        if self.execution_time is None:
-            return None
-
-        minutes, seconds = divmod(self.execution_time.total_seconds(), 60)
-
-        return f"{int(minutes)} minutes, {seconds:.2f} seconds"
+    @staticmethod
+    def elapsed_time_expression():
+        """
+        A queryset expression mirroring the `elapsed_time` property, for use in ordering and
+        filtering. Resolves to null for jobs which have not yet started.
+        """
+        return Coalesce(
+            'execution_time',
+            ExpressionWrapper(Now() - F('started'), output_field=models.DurationField()),
+        )
 
     def delete(self, *args, **kwargs):
         # Use the stored queue name, or fall back to get_queue_for_model for legacy jobs

+ 8 - 7
netbox/core/tables/jobs.py

@@ -1,5 +1,4 @@
 import django_tables2 as tables
-from django.db.models import F
 from django.utils.html import conditional_escape, format_html
 from django.utils.safestring import mark_safe
 from django.utils.translation import gettext_lazy as _
@@ -79,7 +78,7 @@ class JobTable(NetBoxTable):
             return self.default
 
         value = humanize_duration(duration)
-        if record.execution_time is None:
+        if not record.completed:
             # The job is still running, so distinguish its (provisional) elapsed time from a final one
             return format_html(
                 '<span class="text-primary" title="{}">{}</span>', _('Still running'), value
@@ -91,13 +90,15 @@ class JobTable(NetBoxTable):
         # Export the raw number of seconds rather than the humanized rendering
         if (duration := record.elapsed_time) is None:
             return None
-        return max(duration.total_seconds(), 0)
+        return round(max(duration.total_seconds(), 0), 3)
 
     def order_execution_time(self, queryset, is_descending):
-        # Jobs with no recorded execution time are sorted last irrespective of the sort direction
-        field = F('execution_time')
-        ordering = field.desc(nulls_last=True) if is_descending else field.asc(nulls_last=True)
-        return queryset.order_by(ordering), True
+        # Order by the value the column actually displays, so that a long-running job is not sorted
+        # as though it had no execution time. Jobs which never started sort last in either
+        # direction, and pk breaks ties to keep pagination stable.
+        elapsed_time = Job.elapsed_time_expression()
+        ordering = elapsed_time.desc(nulls_last=True) if is_descending else elapsed_time.asc(nulls_last=True)
+        return queryset.order_by(ordering, 'pk'), True
 
 
 class JobLogEntryTable(BaseTable):