Retain Job.duration as a deprecated property
Job.duration has been public since 3.4 and is reachable from user-authored export templates as well as plugins, so removing it outright was a silent breaking change. Restore the original implementation verbatim — including the fallback to `created` when a job never started, and the preformatted string — so existing templates keep working, and warn on access. Planned for removal in v5.0, matching the rack legacy fields. Note that elapsed_time deliberately does not reproduce the `created` fallback: measuring from creation conflates queue wait time with execution time, which is what the new field is meant to record. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
c9185e1eb7
commit
4d3871009c
|
|
@ -32,6 +32,9 @@ The date and time at which the job completed (if complete).
|
|||
|
||||
The amount of time the job spent executing, calculated as the difference between its start and completion times. This is populated only once a started job has completed; while a job is still running, NetBox displays the time elapsed since it started instead.
|
||||
|
||||
!!! warning "The duration property is deprecated"
|
||||
The job model's `duration` property, which returned a preformatted string such as `5 minutes, 3.00 seconds`, has been **deprecated** and is planned for removal in NetBox v5.0. Export templates and plugins should reference `elapsed_time` instead, which returns a duration rather than a string and which also reports progress for a job that is still running.
|
||||
|
||||
### User
|
||||
|
||||
The user who created the job.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import logging
|
||||
import uuid
|
||||
import warnings
|
||||
from dataclasses import asdict
|
||||
from datetime import timedelta
|
||||
from functools import partial
|
||||
|
||||
import django_rq
|
||||
|
|
@ -184,17 +186,48 @@ class Job(models.Model):
|
|||
_("Jobs cannot be assigned to this object type ({type}).").format(type=self.object_type)
|
||||
)
|
||||
|
||||
@property
|
||||
def duration(self):
|
||||
"""
|
||||
Deprecated: use `elapsed_time` instead, which reports a timedelta and also covers jobs which
|
||||
are still running. Retained for the benefit of existing export templates and plugins.
|
||||
"""
|
||||
warnings.warn(
|
||||
"Job.duration is deprecated and will be removed in NetBox v5.0; use Job.elapsed_time "
|
||||
"instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
if not self.completed:
|
||||
return None
|
||||
|
||||
start_time = self.started or self.created
|
||||
|
||||
if not start_time:
|
||||
return None
|
||||
|
||||
duration = self.completed - start_time
|
||||
minutes, seconds = divmod(duration.total_seconds(), 60)
|
||||
|
||||
return f'{int(minutes)} minutes, {seconds:.2f} seconds'
|
||||
|
||||
@property
|
||||
def elapsed_time(self):
|
||||
"""
|
||||
The job's recorded execution time, or the time elapsed so far if it is still running.
|
||||
Returns None for a job which has not yet started.
|
||||
Returns None for a job which has not yet started. As this is the value NetBox displays, an
|
||||
anomalous negative duration (which can result from clock skew) is clamped to zero; the
|
||||
stored `execution_time` is left as recorded.
|
||||
"""
|
||||
if self.execution_time is not None:
|
||||
return self.execution_time
|
||||
if self.started and not self.completed:
|
||||
return timezone.now() - self.started
|
||||
return None
|
||||
elapsed = self.execution_time
|
||||
elif self.started and not self.completed:
|
||||
elapsed = timezone.now() - self.started
|
||||
else:
|
||||
return None
|
||||
|
||||
return max(elapsed, timedelta())
|
||||
|
||||
@staticmethod
|
||||
def elapsed_time_expression():
|
||||
|
|
|
|||
|
|
@ -418,6 +418,55 @@ class JobTestCase(TestCase):
|
|||
self.assertIsNone(job.started)
|
||||
self.assertIsNone(job.elapsed_time)
|
||||
|
||||
def test_elapsed_time_clamps_negative_execution_time(self):
|
||||
"""
|
||||
elapsed_time is the value NetBox displays, so an anomalous negative duration (e.g. resulting
|
||||
from clock skew) is floored at zero. The stored execution_time is left as recorded, so that
|
||||
the anomaly remains visible to the API and to exports.
|
||||
"""
|
||||
job = self._make_job(None, JobNotificationChoices.NOTIFICATION_NEVER)
|
||||
job.started = timezone.now()
|
||||
job.completed = timezone.now()
|
||||
job.execution_time = timedelta(seconds=-5)
|
||||
job.status = JobStatusChoices.STATUS_COMPLETED
|
||||
job.save()
|
||||
|
||||
self.assertEqual(job.elapsed_time, timedelta())
|
||||
self.assertEqual(job.execution_time, timedelta(seconds=-5))
|
||||
|
||||
def test_duration_is_deprecated(self):
|
||||
"""
|
||||
Job.duration is retained for existing export templates and plugins, but must warn.
|
||||
"""
|
||||
job = self._make_job(None, JobNotificationChoices.NOTIFICATION_NEVER)
|
||||
job.started = timezone.now() - timedelta(seconds=90)
|
||||
job.completed = job.started + timedelta(seconds=90)
|
||||
job.status = JobStatusChoices.STATUS_COMPLETED
|
||||
job.save()
|
||||
|
||||
with self.assertWarns(DeprecationWarning):
|
||||
self.assertEqual(job.duration, '1 minutes, 30.00 seconds')
|
||||
|
||||
def test_duration_falls_back_to_created(self):
|
||||
"""
|
||||
The deprecated property's original behavior must be preserved: a job which completed without
|
||||
ever starting reports its duration relative to creation.
|
||||
"""
|
||||
job = self._make_job(None, JobNotificationChoices.NOTIFICATION_NEVER)
|
||||
job.started = None
|
||||
job.completed = job.created + timedelta(seconds=30)
|
||||
job.status = JobStatusChoices.STATUS_ERRORED
|
||||
job.save()
|
||||
|
||||
with self.assertWarns(DeprecationWarning):
|
||||
self.assertEqual(job.duration, '0 minutes, 30.00 seconds')
|
||||
|
||||
def test_duration_none_when_not_completed(self):
|
||||
job = self._make_job(None, JobNotificationChoices.NOTIFICATION_NEVER)
|
||||
|
||||
with self.assertWarns(DeprecationWarning):
|
||||
self.assertIsNone(job.duration)
|
||||
|
||||
def test_elapsed_time_none_when_completed_without_execution_time(self):
|
||||
"""
|
||||
A job which completed without recording an execution time (e.g. one predating the field)
|
||||
|
|
|
|||
Loading…
Reference in New Issue