Ensure correct ordering by elapsed time

This commit is contained in:
Jeremy Stretch 2026-08-05 15:43:00 -04:00
parent 0088ebae8c
commit bff5ee605a
3 changed files with 27 additions and 15 deletions

View File

@ -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):

View File

@ -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

View File

@ -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):