Ensure correct ordering by elapsed time
This commit is contained in:
parent
0088ebae8c
commit
bff5ee605a
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Reference in New Issue